diff --git a/rsconcept/backend/apps/library/serializers/data_access.py b/rsconcept/backend/apps/library/serializers/data_access.py index 7c6d9324..5a1a5480 100644 --- a/rsconcept/backend/apps/library/serializers/data_access.py +++ b/rsconcept/backend/apps/library/serializers/data_access.py @@ -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. ''' diff --git a/rsconcept/backend/apps/library/tests/s_views/t_versions.py b/rsconcept/backend/apps/library/tests/s_views/t_versions.py index 2842773a..d5e60454 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_versions.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_versions.py @@ -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' diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 3067e261..85f2c7b2 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -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) diff --git a/rsconcept/backend/apps/library/views/versions.py b/rsconcept/backend/apps/library/views/versions.py index 7ce06bf8..8eed4423 100644 --- a/rsconcept/backend/apps/library/views/versions.py +++ b/rsconcept/backend/apps/library/views/versions.py @@ -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) diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 5d7c997b..b217a718 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -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()] diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 37988d27..dea094b0 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -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) diff --git a/rsconcept/backend/apps/rsform/models/Constituenta.py b/rsconcept/backend/apps/rsform/models/Constituenta.py index 6286023d..17958414 100644 --- a/rsconcept/backend/apps/rsform/models/Constituenta.py +++ b/rsconcept/backend/apps/rsform/models/Constituenta.py @@ -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 diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index 640cd10c..f0ca51c3 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -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']) diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 9aaf569b..08a7abf6 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -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({ diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py index 187bc021..c65696b0 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py @@ -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') diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index e4773c45..1230faab 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -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() diff --git a/rsconcept/backend/shared/DBTester.py b/rsconcept/backend/shared/DBTester.py new file mode 100644 index 00000000..7e86939b --- /dev/null +++ b/rsconcept/backend/shared/DBTester.py @@ -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) diff --git a/rsconcept/backend/shared/EndpointTester.py b/rsconcept/backend/shared/EndpointTester.py index 98d8bfe1..1b010692 100644 --- a/rsconcept/backend/shared/EndpointTester.py +++ b/rsconcept/backend/shared/EndpointTester.py @@ -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: