mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
R: RSForm cache and transaction.atomic
Some checks are pending
Backend CI / build (3.12) (push) Waiting to run
Some checks are pending
Backend CI / build (3.12) (push) Waiting to run
This commit is contained in:
parent
30a80de424
commit
ec358911fb
|
@ -36,7 +36,7 @@ class LibraryItemSerializer(serializers.ModelSerializer):
|
|||
|
||||
class LibraryItemCloneSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: LibraryItem cloning. '''
|
||||
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
|
||||
items = PKField(many=True, required=False, queryset=Constituenta.objects.all().only('pk'))
|
||||
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
|
|
|
@ -142,7 +142,7 @@ class TestVersionViews(EndpointTester):
|
|||
version_id = self._create_version(data=data)
|
||||
invalid_id = version_id + 1337
|
||||
|
||||
d1.delete()
|
||||
self.owned.delete_cst([d1])
|
||||
x3 = self.owned.insert_new('X3')
|
||||
x1.order = x3.order
|
||||
x1.convention = 'Test2'
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import cast
|
|||
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import generics
|
||||
from rest_framework import status as c
|
||||
|
@ -79,7 +80,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='clone')
|
||||
def clone(self, request: Request, pk):
|
||||
def clone(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Create deep copy of library item. '''
|
||||
serializer = s.LibraryItemCloneSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -139,7 +140,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
},
|
||||
)
|
||||
@action(detail=True, methods=['delete'])
|
||||
def unsubscribe(self, request: Request, pk):
|
||||
def unsubscribe(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Unsubscribe current user from item. '''
|
||||
item = self._get_item()
|
||||
m.Subscription.unsubscribe(user=cast(int, self.request.user.pk), item=item.pk)
|
||||
|
@ -156,7 +157,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='set-owner')
|
||||
def set_owner(self, request: Request, pk):
|
||||
def set_owner(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Set item owner. '''
|
||||
item = self._get_item()
|
||||
serializer = s.UserTargetSerializer(data=request.data)
|
||||
|
@ -188,7 +189,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='set-location')
|
||||
def set_location(self, request: Request, pk):
|
||||
def set_location(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Set item location. '''
|
||||
item = self._get_item()
|
||||
serializer = s.LocationSerializer(data=request.data)
|
||||
|
@ -222,7 +223,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='set-access-policy')
|
||||
def set_access_policy(self, request: Request, pk):
|
||||
def set_access_policy(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Set item AccessPolicy. '''
|
||||
item = self._get_item()
|
||||
serializer = s.AccessPolicySerializer(data=request.data)
|
||||
|
@ -253,7 +254,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='set-editors')
|
||||
def set_editors(self, request: Request, pk):
|
||||
def set_editors(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Set list of editors for item. '''
|
||||
item = self._get_item()
|
||||
serializer = s.UsersListSerializer(data=request.data)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
''' Endpoints for versions. '''
|
||||
from typing import cast
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import generics
|
||||
|
@ -40,11 +41,12 @@ class VersionViewset(
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='restore')
|
||||
def restore(self, request: Request, pk):
|
||||
def restore(self, request: Request, pk) -> HttpResponse:
|
||||
''' Restore version data into current item. '''
|
||||
version = cast(m.Version, self.get_object())
|
||||
item = cast(m.LibraryItem, version.item)
|
||||
RSFormSerializer(item).restore_from_version(version.data)
|
||||
with transaction.atomic():
|
||||
RSFormSerializer(item).restore_from_version(version.data)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=RSFormParseSerializer(item).data
|
||||
|
@ -61,7 +63,7 @@ class VersionViewset(
|
|||
}
|
||||
)
|
||||
@api_view(['GET'])
|
||||
def export_file(request: Request, pk: int):
|
||||
def export_file(request: Request, pk: int) -> HttpResponse:
|
||||
''' Endpoint: Download Exteor compatible file for versioned data. '''
|
||||
try:
|
||||
version = m.Version.objects.get(pk=pk)
|
||||
|
@ -88,7 +90,7 @@ def export_file(request: Request, pk: int):
|
|||
)
|
||||
@api_view(['POST'])
|
||||
@permission_classes([permissions.GlobalUser])
|
||||
def create_version(request: Request, pk_item: int):
|
||||
def create_version(request: Request, pk_item: int) -> HttpResponse:
|
||||
''' Endpoint: Create new version for RSForm copying current content. '''
|
||||
try:
|
||||
item = m.LibraryItem.objects.get(pk=pk_item)
|
||||
|
@ -125,7 +127,7 @@ def create_version(request: Request, pk_item: int):
|
|||
}
|
||||
)
|
||||
@api_view(['GET'])
|
||||
def retrieve_version(request: Request, pk_item: int, pk_version: int):
|
||||
def retrieve_version(request: Request, pk_item: int, pk_version: int) -> HttpResponse:
|
||||
''' Endpoint: Retrieve version for RSForm. '''
|
||||
try:
|
||||
item = m.LibraryItem.objects.get(pk=pk_item)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
''' Models: OSS API. '''
|
||||
from typing import Optional
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from apps.library.models import Editor, LibraryItem, LibraryItemType
|
||||
|
@ -31,11 +30,11 @@ class OperationSchema:
|
|||
model = LibraryItem.objects.get(pk=pk)
|
||||
return OperationSchema(model)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
''' Save wrapper. '''
|
||||
self.model.save(*args, **kwargs)
|
||||
|
||||
def refresh_from_db(self):
|
||||
def refresh_from_db(self) -> None:
|
||||
''' Model wrapper. '''
|
||||
self.model.refresh_from_db()
|
||||
|
||||
|
@ -59,7 +58,7 @@ class OperationSchema:
|
|||
location=self.model.location
|
||||
)
|
||||
|
||||
def update_positions(self, data: list[dict]):
|
||||
def update_positions(self, data: list[dict]) -> None:
|
||||
''' Update positions. '''
|
||||
lookup = {x['id']: x for x in data}
|
||||
operations = self.operations()
|
||||
|
@ -69,7 +68,6 @@ class OperationSchema:
|
|||
item.position_y = lookup[item.pk]['position_y']
|
||||
Operation.objects.bulk_update(operations, ['position_x', 'position_y'])
|
||||
|
||||
@transaction.atomic
|
||||
def create_operation(self, **kwargs) -> Operation:
|
||||
''' Insert new operation. '''
|
||||
result = Operation.objects.create(oss=self.model, **kwargs)
|
||||
|
@ -77,7 +75,6 @@ class OperationSchema:
|
|||
result.refresh_from_db()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
def delete_operation(self, operation: Operation):
|
||||
''' Delete operation. '''
|
||||
operation.delete()
|
||||
|
@ -87,8 +84,7 @@ class OperationSchema:
|
|||
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def set_input(self, target: Operation, schema: Optional[LibraryItem]):
|
||||
def set_input(self, target: Operation, schema: Optional[LibraryItem]) -> None:
|
||||
''' Set input schema for operation. '''
|
||||
if schema == target.result:
|
||||
return
|
||||
|
@ -104,8 +100,7 @@ class OperationSchema:
|
|||
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def set_arguments(self, operation: Operation, arguments: list[Operation]):
|
||||
def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None:
|
||||
''' Set arguments to operation. '''
|
||||
processed: list[Operation] = []
|
||||
changed = False
|
||||
|
@ -125,8 +120,7 @@ class OperationSchema:
|
|||
# TODO: trigger on_change effects
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def set_substitutions(self, target: Operation, substitutes: list[dict]):
|
||||
def set_substitutions(self, target: Operation, substitutes: list[dict]) -> None:
|
||||
''' Clear all arguments for operation. '''
|
||||
processed: list[dict] = []
|
||||
changed = False
|
||||
|
@ -157,7 +151,6 @@ class OperationSchema:
|
|||
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def create_input(self, operation: Operation) -> RSForm:
|
||||
''' Create input RSForm. '''
|
||||
schema = RSForm.create(
|
||||
|
@ -175,7 +168,6 @@ class OperationSchema:
|
|||
self.save()
|
||||
return schema
|
||||
|
||||
@transaction.atomic
|
||||
def execute_operation(self, operation: Operation) -> bool:
|
||||
''' Execute target operation. '''
|
||||
schemas: list[LibraryItem] = [arg.argument.result for arg in operation.getArguments()]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from typing import cast
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import generics, serializers
|
||||
from rest_framework import status as c
|
||||
|
@ -61,7 +62,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['get'], url_path='details')
|
||||
def details(self, request: Request, pk):
|
||||
def details(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Detailed OSS data. '''
|
||||
serializer = s.OperationSchemaSerializer(self._get_item())
|
||||
return Response(
|
||||
|
@ -80,7 +81,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='update-positions')
|
||||
def update_positions(self, request: Request, pk):
|
||||
def update_positions(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Update operations positions. '''
|
||||
serializer = s.PositionsSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -99,7 +100,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='create-operation')
|
||||
def create_operation(self, request: Request, pk):
|
||||
def create_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create new operation. '''
|
||||
serializer = s.OperationCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -135,7 +136,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='delete-operation')
|
||||
def delete_operation(self, request: Request, pk):
|
||||
def delete_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete operation. '''
|
||||
serializer = s.OperationTargetSerializer(
|
||||
data=request.data,
|
||||
|
@ -165,7 +166,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='create-input')
|
||||
def create_input(self, request: Request, pk):
|
||||
def create_input(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create new input RSForm. '''
|
||||
serializer = s.OperationTargetSerializer(
|
||||
data=request.data,
|
||||
|
@ -208,7 +209,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='set-input')
|
||||
def set_input(self, request: Request, pk):
|
||||
def set_input(self, request: Request, pk) -> HttpResponse:
|
||||
''' Set input schema for target operation. '''
|
||||
serializer = s.SetOperationInputSerializer(
|
||||
data=request.data,
|
||||
|
@ -238,7 +239,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='update-operation')
|
||||
def update_operation(self, request: Request, pk):
|
||||
def update_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update operation arguments and parameters. '''
|
||||
serializer = s.OperationUpdateSerializer(
|
||||
data=request.data,
|
||||
|
@ -283,7 +284,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='execute-operation')
|
||||
def execute_operation(self, request: Request, pk):
|
||||
def execute_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Execute operation. '''
|
||||
serializer = s.OperationTargetSerializer(
|
||||
data=request.data,
|
||||
|
@ -323,7 +324,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
}
|
||||
)
|
||||
@action(detail=False, methods=['post'], url_path='get-predecessor')
|
||||
def get_predecessor(self, request: Request):
|
||||
def get_predecessor(self, request: Request) -> HttpResponse:
|
||||
''' Get predecessor. '''
|
||||
# TODO: add tests for this method
|
||||
serializer = CstTargetSerializer(data=request.data)
|
||||
|
|
|
@ -99,8 +99,6 @@ class Constituenta(Model):
|
|||
|
||||
def set_term_resolved(self, new_term: str):
|
||||
''' Set term and reset forms if needed. '''
|
||||
if new_term == self.term_resolved:
|
||||
return
|
||||
self.term_resolved = new_term
|
||||
self.term_forms = []
|
||||
|
||||
|
@ -113,10 +111,6 @@ class Constituenta(Model):
|
|||
if expression != self.definition_formal:
|
||||
modified = True
|
||||
self.definition_formal = expression
|
||||
convention = apply_pattern(self.convention, mapping, _GLOBAL_ID_PATTERN)
|
||||
if convention != self.convention:
|
||||
modified = True
|
||||
self.convention = convention
|
||||
term = apply_pattern(self.term_raw, mapping, _REF_ENTITY_PATTERN)
|
||||
if term != self.term_raw:
|
||||
modified = True
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
''' Models: RSForm API. '''
|
||||
from copy import deepcopy
|
||||
from typing import Optional, cast
|
||||
from typing import Iterable, Optional, cast
|
||||
|
||||
from cctext import Entity, Resolver, TermForm, extract_entities, split_grams
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from apps.library.models import LibraryItem, LibraryItemType, Version
|
||||
|
@ -30,8 +29,71 @@ _INSERT_LAST: int = -1
|
|||
class RSForm:
|
||||
''' RSForm is math form of conceptual schema. '''
|
||||
|
||||
class Cache:
|
||||
''' Cache for RSForm constituents. '''
|
||||
|
||||
def __init__(self, schema: 'RSForm'):
|
||||
self._schema = schema
|
||||
self.constituents: list[Constituenta] = []
|
||||
self.by_id: dict[int, Constituenta] = {}
|
||||
self.by_alias: dict[str, Constituenta] = {}
|
||||
self.is_loaded = False
|
||||
|
||||
def reload(self) -> None:
|
||||
self.constituents = list(
|
||||
self._schema.constituents().only(
|
||||
'order',
|
||||
'alias',
|
||||
'cst_type',
|
||||
'definition_formal',
|
||||
'term_raw',
|
||||
'definition_raw'
|
||||
).order_by('order')
|
||||
)
|
||||
self.by_id = {cst.pk: cst for cst in self.constituents}
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
self.is_loaded = True
|
||||
|
||||
def ensure(self) -> None:
|
||||
if not self.is_loaded:
|
||||
self.reload()
|
||||
|
||||
def clear(self) -> None:
|
||||
self.constituents = []
|
||||
self.by_id = {}
|
||||
self.by_alias = {}
|
||||
self.is_loaded = False
|
||||
|
||||
def insert(self, cst: Constituenta) -> None:
|
||||
if self.is_loaded:
|
||||
self.constituents.insert(cst.order - 1, cst)
|
||||
self.by_id[cst.pk] = cst
|
||||
self.by_alias[cst.alias] = cst
|
||||
|
||||
def insert_multi(self, items: Iterable[Constituenta]) -> None:
|
||||
if self.is_loaded:
|
||||
for cst in items:
|
||||
self.constituents.insert(cst.order - 1, cst)
|
||||
self.by_id[cst.pk] = cst
|
||||
self.by_alias[cst.alias] = cst
|
||||
|
||||
def remove(self, target: Constituenta) -> None:
|
||||
if self.is_loaded:
|
||||
self.constituents.remove(target)
|
||||
del self.by_id[target.pk]
|
||||
del self.by_alias[target.alias]
|
||||
|
||||
def remove_multi(self, target: Iterable[Constituenta]) -> None:
|
||||
if self.is_loaded:
|
||||
for cst in target:
|
||||
self.constituents.remove(cst)
|
||||
del self.by_id[cst.pk]
|
||||
del self.by_alias[cst.alias]
|
||||
|
||||
|
||||
def __init__(self, model: LibraryItem):
|
||||
self.model = model
|
||||
self.cache: RSForm.Cache = RSForm.Cache(self)
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs) -> 'RSForm':
|
||||
|
@ -45,11 +107,11 @@ class RSForm:
|
|||
model = LibraryItem.objects.get(pk=pk)
|
||||
return RSForm(model)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
''' Model wrapper. '''
|
||||
self.model.save(*args, **kwargs)
|
||||
|
||||
def refresh_from_db(self):
|
||||
def refresh_from_db(self) -> None:
|
||||
''' Model wrapper. '''
|
||||
self.model.refresh_from_db()
|
||||
|
||||
|
@ -60,7 +122,7 @@ class RSForm:
|
|||
def resolver(self) -> Resolver:
|
||||
''' Create resolver for text references based on schema terms. '''
|
||||
result = Resolver({})
|
||||
for cst in self.constituents():
|
||||
for cst in self.constituents().only('alias', 'term_resolved', 'term_forms'):
|
||||
entity = Entity(
|
||||
alias=cst.alias,
|
||||
nominal=cst.term_resolved,
|
||||
|
@ -76,49 +138,53 @@ class RSForm:
|
|||
''' Access semantic information on constituents. '''
|
||||
return SemanticInfo(self)
|
||||
|
||||
@transaction.atomic
|
||||
def on_term_change(self, changed: list[int]):
|
||||
def on_term_change(self, changed: list[int]) -> None:
|
||||
''' Trigger cascade resolutions when term changes. '''
|
||||
self.cache.ensure()
|
||||
graph_terms = self._graph_term()
|
||||
expansion = graph_terms.expand_outputs(changed)
|
||||
expanded_change = changed + expansion
|
||||
update_list: list[Constituenta] = []
|
||||
resolver = self.resolver()
|
||||
if len(expansion) > 0:
|
||||
for cst_id in graph_terms.topological_order():
|
||||
if cst_id not in expansion:
|
||||
continue
|
||||
cst = self.constituents().get(id=cst_id)
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.term_raw)
|
||||
if resolved == cst.term_resolved:
|
||||
if resolved == resolver.context[cst.alias].get_nominal():
|
||||
continue
|
||||
cst.set_term_resolved(resolved)
|
||||
cst.save()
|
||||
update_list.append(cst)
|
||||
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
||||
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
||||
|
||||
graph_defs = self._graph_text()
|
||||
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
|
||||
update_list = []
|
||||
if len(update_defs) == 0:
|
||||
return
|
||||
for cst_id in update_defs:
|
||||
cst = self.constituents().get(id=cst_id)
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.definition_raw)
|
||||
if resolved == cst.definition_resolved:
|
||||
continue
|
||||
cst.definition_resolved = resolved
|
||||
cst.save()
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
|
||||
|
||||
def get_max_index(self, cst_type: CstType) -> int:
|
||||
''' Get maximum alias index for specific CstType. '''
|
||||
result: int = 0
|
||||
items = Constituenta.objects \
|
||||
.filter(schema=self.model, cst_type=cst_type) \
|
||||
.order_by('-alias') \
|
||||
.values_list('alias', flat=True)
|
||||
for alias in items:
|
||||
result = max(result, int(alias[1:]))
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = Constituenta.objects \
|
||||
.filter(schema=self.model, cst_type=cst_type) \
|
||||
.only('alias')
|
||||
else:
|
||||
cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type]
|
||||
for cst in cst_list:
|
||||
result = max(result, int(cst.alias[1:]))
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
||||
''' Create new cst from data. '''
|
||||
if insert_after is None:
|
||||
|
@ -142,11 +208,11 @@ class RSForm:
|
|||
result.definition_resolved = resolver.resolve(result.definition_raw)
|
||||
|
||||
result.save()
|
||||
self.cache.insert(result)
|
||||
self.on_term_change([result.pk])
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
def insert_new(
|
||||
self,
|
||||
alias: str,
|
||||
|
@ -169,17 +235,17 @@ class RSForm:
|
|||
cst_type=cst_type,
|
||||
**kwargs
|
||||
)
|
||||
self.cache.insert(result)
|
||||
self.save()
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
def insert_copy(self, items: list[Constituenta], position: int = _INSERT_LAST) -> list[Constituenta]:
|
||||
''' Insert copy of target constituents updating references. '''
|
||||
count = len(items)
|
||||
if count == 0:
|
||||
return []
|
||||
|
||||
self.cache.ensure()
|
||||
position = self._get_insert_position(position)
|
||||
self._shift_positions(position, count)
|
||||
|
||||
|
@ -200,62 +266,65 @@ class RSForm:
|
|||
cst.order = position
|
||||
cst.alias = mapping[cst.alias]
|
||||
cst.apply_mapping(mapping)
|
||||
cst.save()
|
||||
position = position + 1
|
||||
|
||||
new_cst = Constituenta.objects.bulk_create(result)
|
||||
self.cache.insert_multi(new_cst)
|
||||
self.save()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
def move_cst(self, listCst: list[Constituenta], target: int):
|
||||
def move_cst(self, target: list[Constituenta], destination: int) -> None:
|
||||
''' Move list of constituents to specific position '''
|
||||
count_moved = 0
|
||||
count_top = 0
|
||||
count_bot = 0
|
||||
size = len(listCst)
|
||||
update_list = []
|
||||
for cst in self.constituents().only('order').order_by('order'):
|
||||
if cst not in listCst:
|
||||
if count_top + 1 < target:
|
||||
cst.order = count_top + 1
|
||||
count_top += 1
|
||||
else:
|
||||
cst.order = target + size + count_bot
|
||||
count_bot += 1
|
||||
else:
|
||||
cst.order = target + count_moved
|
||||
size = len(target)
|
||||
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = self.constituents().only('order').order_by('order')
|
||||
else:
|
||||
cst_list = self.cache.constituents
|
||||
for cst in cst_list:
|
||||
if cst in target:
|
||||
cst.order = destination + count_moved
|
||||
count_moved += 1
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
elif count_top + 1 < destination:
|
||||
cst.order = count_top + 1
|
||||
count_top += 1
|
||||
else:
|
||||
cst.order = destination + size + count_bot
|
||||
count_bot += 1
|
||||
Constituenta.objects.bulk_update(cst_list, ['order'])
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def delete_cst(self, listCst):
|
||||
def delete_cst(self, target: Iterable[Constituenta]) -> None:
|
||||
''' Delete multiple constituents. Do not check if listCst are from this schema. '''
|
||||
for cst in listCst:
|
||||
cst.delete()
|
||||
self.cache.remove_multi(target)
|
||||
Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete()
|
||||
self._reset_order()
|
||||
self.resolve_all_text()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def substitute(
|
||||
self,
|
||||
original: Constituenta,
|
||||
substitution: Constituenta
|
||||
):
|
||||
) -> None:
|
||||
''' Execute constituenta substitution. '''
|
||||
assert original.pk != substitution.pk
|
||||
mapping = {original.alias: substitution.alias}
|
||||
self.apply_mapping(mapping)
|
||||
self.cache.remove(self.cache.by_id[original.pk])
|
||||
original.delete()
|
||||
self.on_term_change([substitution.pk])
|
||||
|
||||
def restore_order(self):
|
||||
def restore_order(self) -> None:
|
||||
''' Restore order based on types and term graph. '''
|
||||
manager = _OrderManager(self)
|
||||
manager.restore_order()
|
||||
|
||||
def reset_aliases(self):
|
||||
def reset_aliases(self) -> None:
|
||||
''' Recreate all aliases based on constituents order. '''
|
||||
mapping = self._create_reset_mapping()
|
||||
self.apply_mapping(mapping, change_aliases=True)
|
||||
|
@ -273,33 +342,36 @@ class RSForm:
|
|||
mapping[cst.alias] = alias
|
||||
return mapping
|
||||
|
||||
@transaction.atomic
|
||||
def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False):
|
||||
def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False) -> None:
|
||||
''' Apply rename mapping. '''
|
||||
cst_list = self.constituents().order_by('order')
|
||||
for cst in cst_list:
|
||||
self.cache.ensure()
|
||||
update_list: list[Constituenta] = []
|
||||
for cst in self.cache.constituents:
|
||||
if cst.apply_mapping(mapping, change_aliases):
|
||||
cst.save()
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def resolve_all_text(self):
|
||||
def resolve_all_text(self) -> None:
|
||||
''' Trigger reference resolution for all texts. '''
|
||||
self.cache.ensure()
|
||||
graph_terms = self._graph_term()
|
||||
resolver = Resolver({})
|
||||
update_list: list[Constituenta] = []
|
||||
for cst_id in graph_terms.topological_order():
|
||||
cst = self.constituents().get(id=cst_id)
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.term_raw)
|
||||
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
||||
if resolved != cst.term_resolved:
|
||||
cst.term_resolved = resolved
|
||||
cst.save()
|
||||
for cst in self.constituents():
|
||||
resolved = resolver.resolve(cst.definition_raw)
|
||||
if resolved != cst.definition_resolved:
|
||||
cst.definition_resolved = resolved
|
||||
cst.save()
|
||||
cst.term_resolved = resolved
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
||||
|
||||
for cst in self.cache.constituents:
|
||||
resolved = resolver.resolve(cst.definition_raw)
|
||||
cst.definition_resolved = resolved
|
||||
Constituenta.objects.bulk_update(self.cache.constituents, ['definition_resolved'])
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_version(self, version: str, description: str, data) -> Version:
|
||||
''' Creates version for current state. '''
|
||||
return Version.objects.create(
|
||||
|
@ -309,7 +381,6 @@ class RSForm:
|
|||
data=data
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def produce_structure(self, target: Constituenta, parse: dict) -> list[int]:
|
||||
''' Add constituents for each structural element of the target. '''
|
||||
expressions = generate_structure(
|
||||
|
@ -320,9 +391,10 @@ class RSForm:
|
|||
count_new = len(expressions)
|
||||
if count_new == 0:
|
||||
return []
|
||||
position = target.order + 1
|
||||
self._shift_positions(position, count_new)
|
||||
|
||||
position = target.order + 1
|
||||
self.cache.ensure()
|
||||
self._shift_positions(position, count_new)
|
||||
result = []
|
||||
cst_type = CstType.TERM if len(parse['args']) == 0 else CstType.FUNCTION
|
||||
free_index = self.get_max_index(cst_type) + 1
|
||||
|
@ -339,97 +411,86 @@ class RSForm:
|
|||
free_index = free_index + 1
|
||||
position = position + 1
|
||||
|
||||
self.cache.clear()
|
||||
self.save()
|
||||
return result
|
||||
|
||||
def _shift_positions(self, start: int, shift: int):
|
||||
def _shift_positions(self, start: int, shift: int) -> None:
|
||||
if shift == 0:
|
||||
return
|
||||
update_list = \
|
||||
Constituenta.objects \
|
||||
.only('order') \
|
||||
.filter(schema=self.model, order__gte=start)
|
||||
update_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
update_list = Constituenta.objects \
|
||||
.only('order') \
|
||||
.filter(schema=self.model, order__gte=start)
|
||||
else:
|
||||
update_list = [cst for cst in self.cache.constituents if cst.order >= start]
|
||||
for cst in update_list:
|
||||
cst.order += shift
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
|
||||
def _get_last_position(self):
|
||||
if self.constituents().exists():
|
||||
return self.constituents().count()
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _get_insert_position(self, position: int) -> int:
|
||||
if position <= 0 and position != _INSERT_LAST:
|
||||
raise ValidationError(msg.invalidPosition())
|
||||
lastPosition = self._get_last_position()
|
||||
lastPosition = self.constituents().count()
|
||||
if position == _INSERT_LAST:
|
||||
position = lastPosition + 1
|
||||
else:
|
||||
position = max(1, min(position, lastPosition + 1))
|
||||
return position
|
||||
|
||||
@transaction.atomic
|
||||
def _reset_order(self):
|
||||
def _reset_order(self) -> None:
|
||||
order = 1
|
||||
for cst in self.constituents().only('order').order_by('order'):
|
||||
changed: list[Constituenta] = []
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = self.constituents().only('order').order_by('order')
|
||||
else:
|
||||
cst_list = self.cache.constituents
|
||||
for cst in cst_list:
|
||||
if cst.order != order:
|
||||
cst.order = order
|
||||
cst.save()
|
||||
changed.append(cst)
|
||||
order += 1
|
||||
Constituenta.objects.bulk_update(changed, ['order'])
|
||||
|
||||
def _graph_formal(self) -> Graph[int]:
|
||||
''' Graph based on formal definitions. '''
|
||||
self.cache.ensure()
|
||||
result: Graph[int] = Graph()
|
||||
cst_list = \
|
||||
self.constituents() \
|
||||
.only('alias', 'definition_formal') \
|
||||
.order_by('order')
|
||||
for cst in cst_list:
|
||||
for cst in self.cache.constituents:
|
||||
result.add_node(cst.pk)
|
||||
for cst in cst_list:
|
||||
for cst in self.cache.constituents:
|
||||
for alias in extract_globals(cst.definition_formal):
|
||||
try:
|
||||
child = cst_list.get(alias=alias)
|
||||
child = self.cache.by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
except Constituenta.DoesNotExist:
|
||||
pass
|
||||
return result
|
||||
|
||||
def _graph_term(self) -> Graph[int]:
|
||||
''' Graph based on term texts. '''
|
||||
self.cache.ensure()
|
||||
result: Graph[int] = Graph()
|
||||
cst_list = \
|
||||
self.constituents() \
|
||||
.only('alias', 'term_raw') \
|
||||
.order_by('order')
|
||||
for cst in cst_list:
|
||||
for cst in self.cache.constituents:
|
||||
result.add_node(cst.pk)
|
||||
for cst in cst_list:
|
||||
for cst in self.cache.constituents:
|
||||
for alias in extract_entities(cst.term_raw):
|
||||
try:
|
||||
child = cst_list.get(alias=alias)
|
||||
child = self.cache.by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
except Constituenta.DoesNotExist:
|
||||
pass
|
||||
return result
|
||||
|
||||
def _graph_text(self) -> Graph[int]:
|
||||
''' Graph based on definition texts. '''
|
||||
self.cache.ensure()
|
||||
result: Graph[int] = Graph()
|
||||
cst_list = \
|
||||
self.constituents() \
|
||||
.only('alias', 'definition_raw') \
|
||||
.order_by('order')
|
||||
for cst in cst_list:
|
||||
for cst in self.cache.constituents:
|
||||
result.add_node(cst.pk)
|
||||
for cst in cst_list:
|
||||
for cst in self.cache.constituents:
|
||||
for alias in extract_entities(cst.definition_raw):
|
||||
try:
|
||||
child = cst_list.get(alias=alias)
|
||||
child = self.cache.by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
except Constituenta.DoesNotExist:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
|
@ -437,14 +498,11 @@ class SemanticInfo:
|
|||
''' Semantic information derived from constituents. '''
|
||||
|
||||
def __init__(self, schema: RSForm):
|
||||
schema.cache.ensure()
|
||||
self._graph = schema._graph_formal()
|
||||
self._items = list(
|
||||
schema.constituents()
|
||||
.only('alias', 'cst_type', 'definition_formal')
|
||||
.order_by('order')
|
||||
)
|
||||
self._cst_by_alias = {cst.alias: cst for cst in self._items}
|
||||
self._cst_by_ID = {cst.pk: cst for cst in self._items}
|
||||
self._items = schema.cache.constituents
|
||||
self._cst_by_ID = schema.cache.by_id
|
||||
self._cst_by_alias = schema.cache.by_alias
|
||||
self.info = {
|
||||
cst.pk: {
|
||||
'is_simple': False,
|
||||
|
@ -452,7 +510,7 @@ class SemanticInfo:
|
|||
'parent': cst.pk,
|
||||
'children': []
|
||||
}
|
||||
for cst in self._items
|
||||
for cst in schema.cache.constituents
|
||||
}
|
||||
self._calculate_attributes()
|
||||
|
||||
|
@ -475,7 +533,7 @@ class SemanticInfo:
|
|||
''' Access "children" attribute. '''
|
||||
return cast(list[int], self.info[target]['children'])
|
||||
|
||||
def _calculate_attributes(self):
|
||||
def _calculate_attributes(self) -> None:
|
||||
for cst_id in self._graph.topological_order():
|
||||
cst = self._cst_by_ID[cst_id]
|
||||
self.info[cst_id]['is_template'] = infer_template(cst.definition_formal)
|
||||
|
@ -485,7 +543,7 @@ class SemanticInfo:
|
|||
parent = self._infer_parent(cst)
|
||||
self.info[cst_id]['parent'] = parent
|
||||
if parent != cst_id:
|
||||
self.info[parent]['children'].append(cst_id)
|
||||
cast(list[int], self.info[parent]['children']).append(cst_id)
|
||||
|
||||
def _infer_simple_expression(self, target: Constituenta) -> bool:
|
||||
if target.cst_type == CstType.STRUCTURED or is_base_set(target.cst_type):
|
||||
|
@ -565,12 +623,8 @@ class _OrderManager:
|
|||
def __init__(self, schema: RSForm):
|
||||
self._semantic = schema.semantic()
|
||||
self._graph = schema._graph_formal()
|
||||
self._items = list(
|
||||
schema.constituents()
|
||||
.only('order', 'alias', 'cst_type', 'definition_formal')
|
||||
.order_by('order')
|
||||
)
|
||||
self._cst_by_ID = {cst.pk: cst for cst in self._items}
|
||||
self._items = schema.cache.constituents
|
||||
self._cst_by_ID = schema.cache.by_id
|
||||
|
||||
def restore_order(self) -> None:
|
||||
''' Implement order restoration process. '''
|
||||
|
@ -615,10 +669,9 @@ class _OrderManager:
|
|||
result.append(child)
|
||||
self._items = result
|
||||
|
||||
@transaction.atomic
|
||||
def _save_order(self) -> None:
|
||||
order = 1
|
||||
for cst in self._items:
|
||||
cst.order = order
|
||||
cst.save()
|
||||
order += 1
|
||||
Constituenta.objects.bulk_update(self._items, ['order'])
|
||||
|
|
|
@ -3,7 +3,6 @@ from typing import Optional, cast
|
|||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import PrimaryKeyRelatedField as PKField
|
||||
|
@ -72,7 +71,12 @@ class CstDetailsSerializer(serializers.ModelSerializer):
|
|||
|
||||
class CstCreateSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: Constituenta creation. '''
|
||||
insert_after = serializers.IntegerField(required=False, allow_null=True)
|
||||
insert_after = PKField(
|
||||
many=False,
|
||||
allow_null=True,
|
||||
required=False,
|
||||
queryset=Constituenta.objects.all().only('schema_id', 'order')
|
||||
)
|
||||
alias = serializers.CharField(max_length=8)
|
||||
cst_type = serializers.ChoiceField(CstType.choices)
|
||||
|
||||
|
@ -149,7 +153,6 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
result['version'] = version
|
||||
return result | data
|
||||
|
||||
@transaction.atomic
|
||||
def restore_from_version(self, data: dict):
|
||||
''' Load data from version. '''
|
||||
schema = RSForm(cast(LibraryItem, self.instance))
|
||||
|
@ -312,9 +315,9 @@ class CstSubstituteSerializer(serializers.Serializer):
|
|||
raise serializers.ValidationError({
|
||||
f'{original_cst.pk}': msg.substituteDouble(original_cst.alias)
|
||||
})
|
||||
if original_cst.alias == substitution_cst.alias:
|
||||
if original_cst.pk == substitution_cst.pk:
|
||||
raise serializers.ValidationError({
|
||||
'alias': msg.substituteTrivial(original_cst.alias)
|
||||
'original': msg.substituteTrivial(original_cst.alias)
|
||||
})
|
||||
if original_cst.schema_id != schema.pk:
|
||||
raise serializers.ValidationError({
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
''' Testing models: api_RSForm. '''
|
||||
from django.forms import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||
from apps.users.models import User
|
||||
from shared.DBTester import DBTester
|
||||
|
||||
|
||||
class TestRSForm(TestCase):
|
||||
class TestRSForm(DBTester):
|
||||
''' Testing RSForm wrapper. '''
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
self.user2 = User.objects.create(username='User2')
|
||||
self.schema = RSForm.create(title='Test')
|
||||
|
@ -180,7 +181,6 @@ class TestRSForm(TestCase):
|
|||
alias='D1',
|
||||
definition_formal='X1 = X11 = X2',
|
||||
definition_raw='@{X11|sing}',
|
||||
convention='X1',
|
||||
term_raw='@{X1|plur}'
|
||||
)
|
||||
|
||||
|
@ -188,7 +188,6 @@ class TestRSForm(TestCase):
|
|||
d1.refresh_from_db()
|
||||
self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression')
|
||||
self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition')
|
||||
self.assertEqual(d1.convention, 'X3', msg='Map IDs in convention')
|
||||
self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term')
|
||||
self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping')
|
||||
self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping')
|
||||
|
@ -320,7 +319,6 @@ class TestRSForm(TestCase):
|
|||
x2 = self.schema.insert_new('X21')
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D11',
|
||||
convention='D11 - cool',
|
||||
definition_formal='X21=X21',
|
||||
term_raw='@{X21|sing}',
|
||||
definition_raw='@{X11|datv}',
|
||||
|
@ -335,7 +333,6 @@ class TestRSForm(TestCase):
|
|||
self.assertEqual(x1.alias, 'X1')
|
||||
self.assertEqual(x2.alias, 'X2')
|
||||
self.assertEqual(d1.alias, 'D1')
|
||||
self.assertEqual(d1.convention, 'D1 - cool')
|
||||
self.assertEqual(d1.term_raw, '@{X2|sing}')
|
||||
self.assertEqual(d1.definition_raw, '@{X1|datv}')
|
||||
self.assertEqual(d1.definition_resolved, 'test')
|
||||
|
|
|
@ -74,22 +74,20 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='create-cst')
|
||||
def create_cst(self, request: Request, pk):
|
||||
def create_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create new constituenta. '''
|
||||
schema = self._get_item()
|
||||
serializer = s.CstCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
if 'insert_after' in data and data['insert_after'] is not None:
|
||||
try:
|
||||
insert_after = m.Constituenta.objects.get(pk=data['insert_after'])
|
||||
except LibraryItem.DoesNotExist:
|
||||
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||
else:
|
||||
if 'insert_after' not in data:
|
||||
insert_after = None
|
||||
new_cst = m.RSForm(schema).create_cst(data, insert_after)
|
||||
else:
|
||||
insert_after = data['insert_after']
|
||||
|
||||
with transaction.atomic():
|
||||
new_cst = m.RSForm(schema).create_cst(data, insert_after)
|
||||
|
||||
schema.refresh_from_db()
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
|
@ -110,7 +108,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='update-cst')
|
||||
def update_cst(self, request: Request, pk):
|
||||
def update_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update persistent attributes of a given constituenta. '''
|
||||
schema = self._get_item()
|
||||
serializer = s.CstSerializer(data=request.data, partial=True)
|
||||
|
@ -140,7 +138,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='produce-structure')
|
||||
def produce_structure(self, request: Request, pk):
|
||||
def produce_structure(self, request: Request, pk) -> HttpResponse:
|
||||
''' Produce a term for every element of the target constituenta typification. '''
|
||||
schema = self._get_item()
|
||||
|
||||
|
@ -159,8 +157,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
status=c.HTTP_400_BAD_REQUEST,
|
||||
data={f'{cst.pk}': msg.constituentaNoStructure()}
|
||||
)
|
||||
|
||||
result = m.RSForm(schema).produce_structure(cst, cst_parse)
|
||||
with transaction.atomic():
|
||||
result = m.RSForm(schema).produce_structure(cst, cst_parse)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data={
|
||||
|
@ -181,21 +179,20 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='rename-cst')
|
||||
def rename_cst(self, request: Request, pk):
|
||||
def rename_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Rename constituenta possibly changing type. '''
|
||||
schema = self._get_item()
|
||||
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||
old_alias = cst.alias
|
||||
|
||||
mapping = {cst.alias: serializer.validated_data['alias']}
|
||||
cst.alias = serializer.validated_data['alias']
|
||||
cst.cst_type = serializer.validated_data['cst_type']
|
||||
|
||||
with transaction.atomic():
|
||||
cst.save()
|
||||
m.RSForm(schema).apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
|
||||
m.RSForm(schema).apply_mapping(mapping=mapping, change_aliases=False)
|
||||
|
||||
schema.refresh_from_db()
|
||||
cst.refresh_from_db()
|
||||
|
@ -219,7 +216,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='substitute')
|
||||
def substitute(self, request: Request, pk):
|
||||
def substitute(self, request: Request, pk) -> HttpResponse:
|
||||
''' Substitute occurrences of constituenta with another one. '''
|
||||
schema = self._get_item()
|
||||
serializer = s.CstSubstituteSerializer(
|
||||
|
@ -252,7 +249,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
|
||||
def delete_multiple_cst(self, request: Request, pk):
|
||||
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete multiple constituents. '''
|
||||
schema = self._get_item()
|
||||
serializer = s.CstListSerializer(
|
||||
|
@ -260,9 +257,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
context={'schema': schema}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
m.RSForm(schema).delete_cst(serializer.validated_data['items'])
|
||||
|
||||
schema.refresh_from_db()
|
||||
with transaction.atomic():
|
||||
m.RSForm(schema).delete_cst(serializer.validated_data['items'])
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
|
@ -280,7 +276,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='move-cst')
|
||||
def move_cst(self, request: Request, pk):
|
||||
def move_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Move multiple constituents. '''
|
||||
schema = self._get_item()
|
||||
serializer = s.CstMoveSerializer(
|
||||
|
@ -288,10 +284,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
context={'schema': schema}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
m.RSForm(schema).move_cst(
|
||||
listCst=serializer.validated_data['items'],
|
||||
target=serializer.validated_data['move_to']
|
||||
)
|
||||
with transaction.atomic():
|
||||
m.RSForm(schema).move_cst(
|
||||
target=serializer.validated_data['items'],
|
||||
destination=serializer.validated_data['move_to']
|
||||
)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
|
@ -308,7 +305,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||
def reset_aliases(self, request: Request, pk):
|
||||
def reset_aliases(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Recreate all aliases based on order. '''
|
||||
schema = self._get_item()
|
||||
m.RSForm(schema).reset_aliases()
|
||||
|
@ -328,7 +325,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='restore-order')
|
||||
def restore_order(self, request: Request, pk):
|
||||
def restore_order(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Restore order based on types and term graph. '''
|
||||
schema = self._get_item()
|
||||
m.RSForm(schema).restore_order()
|
||||
|
@ -349,7 +346,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='load-trs')
|
||||
def load_trs(self, request: Request, pk):
|
||||
def load_trs(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Load data from file and replace current schema. '''
|
||||
input_serializer = s.RSFormUploadSerializer(data=request.data)
|
||||
input_serializer.is_valid(raise_exception=True)
|
||||
|
@ -380,7 +377,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['get'], url_path='contents')
|
||||
def contents(self, request: Request, pk):
|
||||
def contents(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: View schema db contents (including constituents). '''
|
||||
serializer = s.RSFormSerializer(self.get_object())
|
||||
return Response(
|
||||
|
@ -398,7 +395,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['get'], url_path='details')
|
||||
def details(self, request: Request, pk):
|
||||
def details(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Detailed schema view including statuses and parse. '''
|
||||
serializer = s.RSFormParseSerializer(self.get_object())
|
||||
return Response(
|
||||
|
@ -416,7 +413,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
},
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='check')
|
||||
def check(self, request: Request, pk):
|
||||
def check(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Check RSLang expression against schema context. '''
|
||||
serializer = s.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -438,7 +435,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='resolve')
|
||||
def resolve(self, request: Request, pk):
|
||||
def resolve(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Resolve references in text against schema terms context. '''
|
||||
serializer = s.TextSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -460,7 +457,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
}
|
||||
)
|
||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||
def export_trs(self, request: Request, pk):
|
||||
def export_trs(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Download Exteor compatible file. '''
|
||||
schema = self._get_item()
|
||||
data = s.RSFormTRSSerializer(m.RSForm(schema)).data
|
||||
|
@ -485,7 +482,7 @@ class TrsImportView(views.APIView):
|
|||
c.HTTP_403_FORBIDDEN: None
|
||||
}
|
||||
)
|
||||
def post(self, request: Request):
|
||||
def post(self, request: Request) -> HttpResponse:
|
||||
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
||||
owner = cast(User, self.request.user)
|
||||
_prepare_rsform_data(data, request, owner)
|
||||
|
@ -512,7 +509,7 @@ class TrsImportView(views.APIView):
|
|||
}
|
||||
)
|
||||
@api_view(['POST'])
|
||||
def create_rsform(request: Request):
|
||||
def create_rsform(request: Request) -> HttpResponse:
|
||||
''' Endpoint: Create RSForm from user input and/or trs file. '''
|
||||
owner = cast(User, request.user) if not request.user.is_anonymous else None
|
||||
if 'file' not in request.FILES:
|
||||
|
@ -564,7 +561,7 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
|
|||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
||||
)
|
||||
@api_view(['PATCH'])
|
||||
def inline_synthesis(request: Request):
|
||||
def inline_synthesis(request: Request) -> HttpResponse:
|
||||
''' Endpoint: Inline synthesis. '''
|
||||
serializer = s.InlineSynthesisSerializer(
|
||||
data=request.data,
|
||||
|
@ -581,10 +578,10 @@ def inline_synthesis(request: Request):
|
|||
original = cast(m.Constituenta, substitution['original'])
|
||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||
if original in items:
|
||||
index = next(i for (i, cst) in enumerate(items) if cst == original)
|
||||
index = next(i for (i, cst) in enumerate(items) if cst.pk == original.pk)
|
||||
original = new_items[index]
|
||||
else:
|
||||
index = next(i for (i, cst) in enumerate(items) if cst == replacement)
|
||||
index = next(i for (i, cst) in enumerate(items) if cst.pk == replacement.pk)
|
||||
replacement = new_items[index]
|
||||
receiver.substitute(original, replacement)
|
||||
receiver.restore_order()
|
||||
|
|
23
rsconcept/backend/shared/DBTester.py
Normal file
23
rsconcept/backend/shared/DBTester.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
''' Utils: tester for database operations. '''
|
||||
import logging
|
||||
|
||||
from django.db import connection
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
class DBTester(APITestCase):
|
||||
''' Abstract base class for Testing database. '''
|
||||
|
||||
def setUp(self):
|
||||
self.logger = logging.getLogger('django.db.backends')
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
def start_db_log(self):
|
||||
''' Warning! Do not use this second time before calling stop_db_log. '''
|
||||
''' Warning! Do not forget to enable global logging in settings. '''
|
||||
logging.disable(logging.NOTSET)
|
||||
connection.force_debug_cursor = True
|
||||
|
||||
def stop_db_log(self):
|
||||
connection.force_debug_cursor = False
|
||||
logging.disable(logging.CRITICAL)
|
|
@ -1,13 +1,12 @@
|
|||
''' Utils: base tester class for endpoints. '''
|
||||
import logging
|
||||
|
||||
from django.db import connection
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient, APIRequestFactory, APITestCase
|
||||
from rest_framework.test import APIClient, APIRequestFactory
|
||||
|
||||
from apps.library.models import Editor, LibraryItem
|
||||
from apps.users.models import User
|
||||
|
||||
from .DBTester import DBTester
|
||||
|
||||
|
||||
def decl_endpoint(endpoint: str, method: str):
|
||||
''' Decorator for EndpointTester methods to provide API attributes. '''
|
||||
|
@ -25,10 +24,11 @@ def decl_endpoint(endpoint: str, method: str):
|
|||
return set_endpoint_inner
|
||||
|
||||
|
||||
class EndpointTester(APITestCase):
|
||||
class EndpointTester(DBTester):
|
||||
''' Abstract base class for Testing endpoints. '''
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create(
|
||||
username='UserTest',
|
||||
|
@ -43,9 +43,6 @@ class EndpointTester(APITestCase):
|
|||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
self.logger = logging.getLogger('django.db.backends')
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
def setUpFullUsers(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create_user(
|
||||
|
@ -77,16 +74,6 @@ class EndpointTester(APITestCase):
|
|||
def logout(self):
|
||||
self.client.logout()
|
||||
|
||||
def start_db_log(self):
|
||||
''' Warning! Do not use this second time before calling stop_db_log. '''
|
||||
''' Warning! Do not forget to enable global logging in settings. '''
|
||||
logging.disable(logging.NOTSET)
|
||||
connection.force_debug_cursor = True
|
||||
|
||||
def stop_db_log(self):
|
||||
connection.force_debug_cursor = False
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
def set_params(self, **kwargs):
|
||||
''' Given named argument values resolve current endpoint_mask. '''
|
||||
if self.endpoint_mask and len(kwargs) > 0:
|
||||
|
|
Loading…
Reference in New Issue
Block a user