From 8697ee61752a603697ba37a250aa9bb94fabd642 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:20:51 +0300 Subject: [PATCH] Refactoring: improving backend --- .dockerignore | 1 + .gitignore | 1 + .vscode/launch.json | 17 +++++ rsconcept/backend/apps/oss/admin.py | 20 ++++- ...002_operationschema_alter_operation_oss.py | 31 ++++++++ rsconcept/backend/apps/oss/models/Argument.py | 2 +- .../backend/apps/oss/models/Operation.py | 2 +- .../models/{api_OSS.py => OperationSchema.py} | 50 +++++++------ .../apps/oss/models/SynthesisSubstitution.py | 2 +- rsconcept/backend/apps/oss/models/__init__.py | 2 +- .../apps/oss/serializers/data_access.py | 11 ++- .../apps/oss/tests/s_models/__init__.py | 3 + .../apps/oss/tests/s_models/t_Argument.py | 36 +++++++++ .../apps/oss/tests/s_models/t_Operation.py | 31 ++++++++ .../tests/s_models/t_SynthesisSubstitution.py | 75 +++++++++++++++++++ .../backend/apps/oss/tests/s_views/t_oss.py | 42 +++++------ rsconcept/backend/apps/oss/views/oss.py | 16 ++-- ...form_alter_constituenta_schema_and_more.py | 35 +++++++++ .../apps/rsform/models/Constituenta.py | 2 +- .../apps/rsform/models/LibraryTemplate.py | 2 +- .../models/{api_RSForm.py => RSForm.py} | 50 +++++++------ .../backend/apps/rsform/models/__init__.py | 2 +- .../apps/rsform/serializers/data_access.py | 42 +++++------ .../apps/rsform/serializers/io_files.py | 24 +++--- .../rsform/tests/s_models/t_Constituenta.py | 6 +- .../apps/rsform/tests/s_models/t_Editor.py | 5 +- .../apps/rsform/tests/s_models/t_RSForm.py | 30 ++++---- .../rsform/tests/s_views/t_constituents.py | 10 +-- .../apps/rsform/tests/s_views/t_library.py | 14 ++-- .../apps/rsform/tests/s_views/t_operations.py | 18 ++--- .../apps/rsform/tests/s_views/t_rsforms.py | 54 ++++++------- .../apps/rsform/tests/s_views/t_versions.py | 13 ++-- .../backend/apps/rsform/views/library.py | 28 +++---- .../backend/apps/rsform/views/operations.py | 4 +- .../backend/apps/rsform/views/rsforms.py | 52 ++++++------- .../backend/apps/rsform/views/versions.py | 16 ++-- rsconcept/backend/project/settings.py | 13 +++- rsconcept/backend/requirements-dev.txt | 1 + rsconcept/backend/shared/messages.py | 4 - scripts/dev/GraphDB.ps1 | 16 ++++ scripts/dev/RunCoverage.ps1 | 4 +- 41 files changed, 530 insertions(+), 257 deletions(-) create mode 100644 rsconcept/backend/apps/oss/migrations/0002_operationschema_alter_operation_oss.py rename rsconcept/backend/apps/oss/models/{api_OSS.py => OperationSchema.py} (79%) create mode 100644 rsconcept/backend/apps/oss/tests/s_models/t_Argument.py create mode 100644 rsconcept/backend/apps/oss/tests/s_models/t_Operation.py create mode 100644 rsconcept/backend/apps/oss/tests/s_models/t_SynthesisSubstitution.py create mode 100644 rsconcept/backend/apps/rsform/migrations/0009_rsform_alter_constituenta_schema_and_more.py rename rsconcept/backend/apps/rsform/models/{api_RSForm.py => RSForm.py} (95%) create mode 100644 scripts/dev/GraphDB.ps1 diff --git a/.dockerignore b/.dockerignore index 181b30a8..9b37f643 100644 --- a/.dockerignore +++ b/.dockerignore @@ -47,6 +47,7 @@ cover/ # Django rsconcept/frontend/static rsconcept/frontend/media +visualizeDB.dot *.log db.sqlite3 db.sqlite3-journal diff --git a/.gitignore b/.gitignore index 291ef36e..45cd2dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ cover/ *.log db.sqlite3 db.sqlite3-journal +visualizeDB.dot # React diff --git a/.vscode/launch.json b/.vscode/launch.json index 9ba8fe0c..a750a697 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,6 +5,7 @@ "version": "0.2.0", "configurations": [ { + // Run Frontend + Backend with current Database "name": "Run", "type": "PowerShell", "request": "launch", @@ -12,6 +13,7 @@ "args": [] }, { + // Run Linters "name": "Lint", "type": "PowerShell", "request": "launch", @@ -19,6 +21,7 @@ "args": [] }, { + // Run Tests "name": "Test", "type": "PowerShell", "request": "launch", @@ -26,6 +29,7 @@ "args": [] }, { + // Run Tests for backend for current file in Debug mode "name": "BE-DebugTestFile", "type": "debugpy", "request": "launch", @@ -35,6 +39,7 @@ "django": true }, { + // Run Tests for frontned in Debug mode "name": "FE-DebugTestAll", "type": "node", "request": "launch", @@ -55,6 +60,7 @@ } }, { + // Run Browser in Debug mode (Backend should be running) "name": "FE-Debug", "type": "chrome", "request": "launch", @@ -62,6 +68,7 @@ "webRoot": "${workspaceFolder}/rsconcept/frontend" }, { + // Run Backend in Debug mode "name": "BE-Debug", "type": "debugpy", "request": "launch", @@ -70,6 +77,7 @@ "django": true }, { + // Run Backend test coverage "name": "BE-Coverage", "type": "PowerShell", "request": "launch", @@ -77,11 +85,20 @@ "args": [] }, { + // Recreate database, fill with initial data and Run Backend + Frontend "name": "Restart", "type": "PowerShell", "request": "launch", "script": "${workspaceFolder}/scripts/dev/RunServer.ps1", "args": ["-freshStart"] + }, + { + // Create DOT file for visualizing database + "name": "BE-GraphDB", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/scripts/dev/GraphDB.ps1", + "args": [] } ] } diff --git a/rsconcept/backend/apps/oss/admin.py b/rsconcept/backend/apps/oss/admin.py index 3bf56a23..813d4bfa 100644 --- a/rsconcept/backend/apps/oss/admin.py +++ b/rsconcept/backend/apps/oss/admin.py @@ -7,8 +7,24 @@ from . import models class OperationAdmin(admin.ModelAdmin): ''' Admin model: Operation. ''' ordering = ['oss'] - list_display = ['oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y'] - search_fields = ['operation_type', 'title', 'alias'] + list_display = ['id', 'oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y'] + search_fields = ['id', 'operation_type', 'title', 'alias'] + + +class ArgumentAdmin(admin.ModelAdmin): + ''' Admin model: Operation arguments. ''' + ordering = ['operation'] + list_display = ['id', 'operation', 'argument'] + search_fields = ['id', 'operation', 'argument'] + + +class SynthesisSubstitutionAdmin(admin.ModelAdmin): + ''' Admin model: Substitutions as part of Synthesis operation. ''' + ordering = ['operation'] + list_display = ['id', 'operation', 'original', 'substitution', 'transfer_term'] + search_fields = ['id', 'operation', 'original', 'substitution'] admin.site.register(models.Operation, OperationAdmin) +admin.site.register(models.Argument, ArgumentAdmin) +admin.site.register(models.SynthesisSubstitution, SynthesisSubstitutionAdmin) diff --git a/rsconcept/backend/apps/oss/migrations/0002_operationschema_alter_operation_oss.py b/rsconcept/backend/apps/oss/migrations/0002_operationschema_alter_operation_oss.py new file mode 100644 index 00000000..c965de6e --- /dev/null +++ b/rsconcept/backend/apps/oss/migrations/0002_operationschema_alter_operation_oss.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.7 on 2024-07-22 13:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oss', '0001_initial'), + ('rsform', '0008_alter_libraryitem_item_type'), + ] + + operations = [ + migrations.CreateModel( + name='OperationSchema', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('rsform.libraryitem',), + ), + migrations.AlterField( + model_name='operation', + name='oss', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='oss.operationschema', verbose_name='Схема синтеза'), + ), + ] diff --git a/rsconcept/backend/apps/oss/models/Argument.py b/rsconcept/backend/apps/oss/models/Argument.py index 3282113d..e8ac9094 100644 --- a/rsconcept/backend/apps/oss/models/Argument.py +++ b/rsconcept/backend/apps/oss/models/Argument.py @@ -24,4 +24,4 @@ class Argument(Model): unique_together = [['operation', 'argument']] def __str__(self) -> str: - return f'{self.argument.pk} -> {self.operation.pk}' + return f'{self.argument} -> {self.operation}' diff --git a/rsconcept/backend/apps/oss/models/Operation.py b/rsconcept/backend/apps/oss/models/Operation.py index 54b16740..03e9e45e 100644 --- a/rsconcept/backend/apps/oss/models/Operation.py +++ b/rsconcept/backend/apps/oss/models/Operation.py @@ -21,7 +21,7 @@ class Operation(Model): ''' Operational schema Unit.''' oss: ForeignKey = ForeignKey( verbose_name='Схема синтеза', - to='rsform.LibraryItem', + to='oss.OperationSchema', on_delete=CASCADE, related_name='items' ) diff --git a/rsconcept/backend/apps/oss/models/api_OSS.py b/rsconcept/backend/apps/oss/models/OperationSchema.py similarity index 79% rename from rsconcept/backend/apps/oss/models/api_OSS.py rename to rsconcept/backend/apps/oss/models/OperationSchema.py index c4a3db7c..ef922c13 100644 --- a/rsconcept/backend/apps/oss/models/api_OSS.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -3,7 +3,7 @@ from typing import Optional from django.core.exceptions import ValidationError from django.db import transaction -from django.db.models import QuerySet +from django.db.models import Manager, QuerySet from apps.rsform.models import LibraryItem, LibraryItemType from shared import messages as msg @@ -13,30 +13,37 @@ from .Operation import Operation from .SynthesisSubstitution import SynthesisSubstitution -class OperationSchema: +class OperationSchema(LibraryItem): ''' Operations schema API. ''' - def __init__(self, item: LibraryItem): - if item.item_type != LibraryItemType.OPERATION_SCHEMA: - raise ValueError(msg.libraryTypeUnexpected()) - self.item = item + class Meta: + ''' Model metadata. ''' + proxy = True - @staticmethod - def create(**kwargs) -> 'OperationSchema': - item = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs) - return OperationSchema(item=item) + class InternalManager(Manager): + ''' Object manager. ''' + + def get_queryset(self) -> QuerySet: + return super().get_queryset().filter(item_type=LibraryItemType.OPERATION_SCHEMA) + + def create(self, **kwargs): + kwargs.update({'item_type': LibraryItemType.OPERATION_SCHEMA}) + return super().create(**kwargs) + + # Legit overriding object manager + objects = InternalManager() # type: ignore[misc] def operations(self) -> QuerySet[Operation]: ''' Get QuerySet containing all operations of current OSS. ''' - return Operation.objects.filter(oss=self.item) + return Operation.objects.filter(oss=self) def arguments(self) -> QuerySet[Argument]: ''' Operation arguments. ''' - return Argument.objects.filter(operation__oss=self.item) + return Argument.objects.filter(operation__oss=self) def substitutions(self) -> QuerySet[SynthesisSubstitution]: ''' Operation substitutions. ''' - return SynthesisSubstitution.objects.filter(operation__oss=self.item) + return SynthesisSubstitution.objects.filter(operation__oss=self) def update_positions(self, data: list[dict]): ''' Update positions. ''' @@ -53,11 +60,8 @@ class OperationSchema: ''' Insert new operation. ''' if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists(): raise ValidationError(msg.aliasTaken(kwargs['alias'])) - result = Operation.objects.create( - oss=self.item, - **kwargs - ) - self.item.save() + result = Operation.objects.create(oss=self, **kwargs) + self.save() result.refresh_from_db() return result @@ -69,7 +73,7 @@ class OperationSchema: # deal with attached schema # trigger on_change effects - self.item.save() + self.save() @transaction.atomic def set_input(self, target: Operation, schema: Optional[LibraryItem]): @@ -87,7 +91,7 @@ class OperationSchema: # trigger on_change effects - self.item.save() + self.save() @transaction.atomic def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]: @@ -95,7 +99,7 @@ class OperationSchema: if Argument.objects.filter(operation=operation, argument=argument).exists(): return None result = Argument.objects.create(operation=operation, argument=argument) - self.item.save() + self.save() return result @transaction.atomic @@ -109,7 +113,7 @@ class OperationSchema: # trigger on_change effects - self.item.save() + self.save() @transaction.atomic def set_substitutions(self, target: Operation, substitutes: list[dict]): @@ -125,4 +129,4 @@ class OperationSchema: # trigger on_change effects - self.item.save() + self.save() diff --git a/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py b/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py index 61c2da22..6dae6329 100644 --- a/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py +++ b/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py @@ -33,4 +33,4 @@ class SynthesisSubstitution(Model): verbose_name_plural = 'Таблицы отождествлений' def __str__(self) -> str: - return f'{self.original.pk} -> {self.substitution.pk}' + return f'{self.original} -> {self.substitution}' diff --git a/rsconcept/backend/apps/oss/models/__init__.py b/rsconcept/backend/apps/oss/models/__init__.py index baa166e3..23435594 100644 --- a/rsconcept/backend/apps/oss/models/__init__.py +++ b/rsconcept/backend/apps/oss/models/__init__.py @@ -2,7 +2,7 @@ from apps.rsform.models import LibraryItem, LibraryItemType -from .api_OSS import OperationSchema from .Argument import Argument from .Operation import Operation, OperationType +from .OperationSchema import OperationSchema from .SynthesisSubstitution import SynthesisSubstitution diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index c187b0c3..74bbd142 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -85,20 +85,19 @@ class OperationSchemaSerializer(serializers.ModelSerializer): class Meta: ''' serializer metadata. ''' - model = LibraryItem + model = OperationSchema fields = '__all__' - def to_representation(self, instance: LibraryItem): + def to_representation(self, instance: OperationSchema): result = LibraryItemDetailsSerializer(instance).data - oss = OperationSchema(instance) result['items'] = [] - for operation in oss.operations(): + for operation in instance.operations(): result['items'].append(OperationSerializer(operation).data) result['arguments'] = [] - for argument in oss.arguments(): + for argument in instance.arguments(): result['arguments'].append(ArgumentSerializer(argument).data) result['substitutions'] = [] - for substitution in oss.substitutions().values( + for substitution in instance.substitutions().values( 'operation', 'original', 'substitution', diff --git a/rsconcept/backend/apps/oss/tests/s_models/__init__.py b/rsconcept/backend/apps/oss/tests/s_models/__init__.py index 4458b11f..f30b6e45 100644 --- a/rsconcept/backend/apps/oss/tests/s_models/__init__.py +++ b/rsconcept/backend/apps/oss/tests/s_models/__init__.py @@ -1 +1,4 @@ ''' Tests for Django Models. ''' +from .t_Argument import * +from .t_Operation import * +from .t_SynthesisSubstitution import * diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_Argument.py b/rsconcept/backend/apps/oss/tests/s_models/t_Argument.py new file mode 100644 index 00000000..8a8a288b --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_models/t_Argument.py @@ -0,0 +1,36 @@ +''' Testing models: Argument. ''' +from django.test import TestCase + +from apps.oss.models import Argument, Operation, OperationSchema, OperationType + + +class TestArgument(TestCase): + ''' Testing Argument model. ''' + + def setUp(self): + self.oss = OperationSchema.objects.create(alias='T1') + + self.operation1 = Operation.objects.create(oss=self.oss, alias='KS1', operation_type=OperationType.INPUT) + self.operation2 = Operation.objects.create(oss=self.oss, alias='KS2', operation_type=OperationType.SYNTHESIS) + self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.INPUT) + self.argument = Argument.objects.create( + operation=self.operation2, + argument=self.operation1 + ) + + + def test_str(self): + testStr = f'{self.operation1} -> {self.operation2}' + self.assertEqual(str(self.argument), testStr) + + + def test_cascade_delete_operation(self): + self.assertEqual(Argument.objects.count(), 1) + self.operation2.delete() + self.assertEqual(Argument.objects.count(), 0) + + + def test_cascade_delete_argument(self): + self.assertEqual(Argument.objects.count(), 1) + self.operation1.delete() + self.assertEqual(Argument.objects.count(), 0) diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py b/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py new file mode 100644 index 00000000..3278bf3e --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py @@ -0,0 +1,31 @@ +''' Testing models: Operation. ''' +from django.test import TestCase + +from apps.oss.models import Operation, OperationSchema, OperationType + + +class TestOperation(TestCase): + ''' Testing Operation model. ''' + + def setUp(self): + self.oss = OperationSchema.objects.create(alias='T1') + self.operation = Operation.objects.create( + oss=self.oss, + alias='KS1' + ) + + + def test_str(self): + testStr = 'Операция KS1' + self.assertEqual(str(self.operation), testStr) + + + def test_create_default(self): + self.assertEqual(self.operation.oss, self.oss) + self.assertEqual(self.operation.operation_type, OperationType.INPUT) + self.assertEqual(self.operation.result, None) + self.assertEqual(self.operation.alias, 'KS1') + self.assertEqual(self.operation.title, '') + self.assertEqual(self.operation.comment, '') + self.assertEqual(self.operation.position_x, 0) + self.assertEqual(self.operation.position_y, 0) diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_SynthesisSubstitution.py b/rsconcept/backend/apps/oss/tests/s_models/t_SynthesisSubstitution.py new file mode 100644 index 00000000..6e871282 --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_models/t_SynthesisSubstitution.py @@ -0,0 +1,75 @@ +''' Testing models: SynthesisSubstitution. ''' +from unittest import result + +from django.test import TestCase + +from apps.oss.models import ( + Argument, + Operation, + OperationSchema, + OperationType, + SynthesisSubstitution +) +from apps.rsform.models import RSForm + + +class TestSynthesisSubstitution(TestCase): + ''' Testing SynthesisSubstitution model. ''' + + def setUp(self): + self.oss = OperationSchema.objects.create(alias='T1') + + self.ks1 = RSForm.objects.create(alias='KS1', title='Test1') + self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1') + self.ks2 = RSForm.objects.create(alias='KS2', title='Test2') + self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2') + + self.operation1 = Operation.objects.create( + oss=self.oss, + alias='KS1', + operation_type=OperationType.INPUT, + result=self.ks1) + self.operation2 = Operation.objects.create( + oss=self.oss, + alias='KS2', + operation_type=OperationType.INPUT, + result=self.ks1) + self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.SYNTHESIS) + Argument.objects.create( + operation=self.operation3, + argument=self.operation1 + ) + Argument.objects.create( + operation=self.operation3, + argument=self.operation2 + ) + + self.substitution = SynthesisSubstitution.objects.create( + operation=self.operation3, + original=self.ks1x1, + substitution=self.ks2x1, + transfer_term=False + ) + + + def test_str(self): + testStr = f'{self.ks1x1} -> {self.ks2x1}' + self.assertEqual(str(self.substitution), testStr) + + + def test_cascade_delete_operation(self): + self.assertEqual(SynthesisSubstitution.objects.count(), 1) + self.operation3.delete() + self.assertEqual(SynthesisSubstitution.objects.count(), 0) + + + def test_cascade_delete_original(self): + self.assertEqual(SynthesisSubstitution.objects.count(), 1) + self.ks1x1.delete() + self.assertEqual(SynthesisSubstitution.objects.count(), 0) + + + def test_cascade_delete_substitution(self): + self.assertEqual(SynthesisSubstitution.objects.count(), 1) + self.ks2x1.delete() + self.assertEqual(SynthesisSubstitution.objects.count(), 0) diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index d28da570..a7bd57c8 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -12,29 +12,29 @@ class TestOssViewset(EndpointTester): def setUp(self): super().setUp() - self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user) - self.owned_id = self.owned.item.pk - self.unowned = OperationSchema.create(title='Test2', alias='T2') - self.unowned_id = self.unowned.item.pk - self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) - self.private_id = self.private.item.pk - self.invalid_id = self.private.item.pk + 1337 + self.owned = OperationSchema.objects.create(title='Test', alias='T1', owner=self.user) + self.owned_id = self.owned.pk + self.unowned = OperationSchema.objects.create(title='Test2', alias='T2') + self.unowned_id = self.unowned.pk + self.private = OperationSchema.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) + self.private_id = self.private.pk + self.invalid_id = self.private.pk + 1337 def populateData(self): - self.ks1 = RSForm.create(alias='KS1', title='Test1') + self.ks1 = RSForm.objects.create(alias='KS1', title='Test1') self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1') - self.ks2 = RSForm.create(alias='KS2', title='Test2') + self.ks2 = RSForm.objects.create(alias='KS2', title='Test2') self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2') self.operation1 = self.owned.create_operation( alias='1', operation_type=OperationType.INPUT, - result=self.ks1.item + result=self.ks1 ) self.operation2 = self.owned.create_operation( alias='2', operation_type=OperationType.INPUT, - result=self.ks2.item + result=self.ks2 ) self.operation3 = self.owned.create_operation( alias='3', @@ -53,12 +53,12 @@ class TestOssViewset(EndpointTester): self.populateData() response = self.executeOK(item=self.owned_id) - self.assertEqual(response.data['owner'], self.owned.item.owner.pk) - self.assertEqual(response.data['title'], self.owned.item.title) - self.assertEqual(response.data['alias'], self.owned.item.alias) - self.assertEqual(response.data['location'], self.owned.item.location) - self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) - self.assertEqual(response.data['visible'], self.owned.item.visible) + self.assertEqual(response.data['owner'], self.owned.owner.pk) + self.assertEqual(response.data['title'], self.owned.title) + self.assertEqual(response.data['alias'], self.owned.alias) + self.assertEqual(response.data['location'], self.owned.location) + self.assertEqual(response.data['access_policy'], self.owned.access_policy) + self.assertEqual(response.data['visible'], self.owned.visible) self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) @@ -179,7 +179,7 @@ class TestOssViewset(EndpointTester): 'arguments': [self.operation1.pk, self.operation3.pk] } response = self.executeCreated(data=data, item=self.owned_id) - self.owned.item.refresh_from_db() + self.owned.refresh_from_db() new_operation = response.data['new_operation'] arguments = self.owned.arguments() self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1)) @@ -193,14 +193,14 @@ class TestOssViewset(EndpointTester): 'item_data': { 'alias': 'Test4', 'operation_type': OperationType.INPUT, - 'result': self.ks1.item.pk + 'result': self.ks1.pk }, 'positions': [], } response = self.executeCreated(data=data, item=self.owned_id) - self.owned.item.refresh_from_db() + self.owned.refresh_from_db() new_operation = response.data['new_operation'] - self.assertEqual(new_operation['result'], self.ks1.item.pk) + self.assertEqual(new_operation['result'], self.ks1.pk) @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 3c7bf2f0..6828d868 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -20,11 +20,11 @@ from .. import serializers as s @extend_schema_view() class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): ''' Endpoint: OperationSchema. ''' - queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.OPERATION_SCHEMA) + queryset = m.OperationSchema.objects.all() serializer_class = s.LibraryItemSerializer def _get_schema(self) -> m.OperationSchema: - return m.OperationSchema(cast(m.LibraryItem, self.get_object())) + return cast(m.OperationSchema, self.get_object()) def get_permissions(self): ''' Determine permission class. ''' @@ -52,7 +52,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['get'], url_path='details') def details(self, request: Request, pk): ''' Endpoint: Detailed OSS data. ''' - serializer = s.OperationSchemaSerializer(cast(m.LibraryItem, self.get_object())) + serializer = s.OperationSchemaSerializer(self._get_schema()) return Response( status=c.HTTP_200_OK, data=serializer.data @@ -101,13 +101,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: for argument in serializer.validated_data['arguments']: schema.add_argument(operation=new_operation, argument=argument) - schema.item.refresh_from_db() + schema.refresh_from_db() response = Response( status=c.HTTP_201_CREATED, data={ 'new_operation': s.OperationSerializer(new_operation).data, - 'oss': s.OperationSchemaSerializer(schema.item).data + 'oss': s.OperationSchemaSerializer(schema).data } ) return response @@ -129,16 +129,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev schema = self._get_schema() serializer = s.OperationDeleteSerializer( data=request.data, - context={'oss': schema.item} + context={'oss': schema} ) serializer.is_valid(raise_exception=True) with transaction.atomic(): schema.update_positions(serializer.validated_data['positions']) schema.delete_operation(serializer.validated_data['target']) - schema.item.refresh_from_db() + schema.refresh_from_db() return Response( status=c.HTTP_200_OK, - data=s.OperationSchemaSerializer(schema.item).data + data=s.OperationSchemaSerializer(schema).data ) diff --git a/rsconcept/backend/apps/rsform/migrations/0009_rsform_alter_constituenta_schema_and_more.py b/rsconcept/backend/apps/rsform/migrations/0009_rsform_alter_constituenta_schema_and_more.py new file mode 100644 index 00000000..d1c5d460 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0009_rsform_alter_constituenta_schema_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.7 on 2024-07-22 14:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0008_alter_libraryitem_item_type'), + ] + + operations = [ + migrations.CreateModel( + name='RSForm', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('rsform.libraryitem',), + ), + migrations.AlterField( + model_name='constituenta', + name='schema', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема'), + ), + migrations.AlterField( + model_name='librarytemplate', + name='lib_source', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Источник'), + ), + ] diff --git a/rsconcept/backend/apps/rsform/models/Constituenta.py b/rsconcept/backend/apps/rsform/models/Constituenta.py index fe09a12f..fac888e3 100644 --- a/rsconcept/backend/apps/rsform/models/Constituenta.py +++ b/rsconcept/backend/apps/rsform/models/Constituenta.py @@ -40,7 +40,7 @@ class Constituenta(Model): ''' Constituenta is the base unit for every conceptual schema. ''' schema: ForeignKey = ForeignKey( verbose_name='Концептуальная схема', - to='rsform.LibraryItem', + to='rsform.RSForm', on_delete=CASCADE ) order: PositiveIntegerField = PositiveIntegerField( diff --git a/rsconcept/backend/apps/rsform/models/LibraryTemplate.py b/rsconcept/backend/apps/rsform/models/LibraryTemplate.py index c49a5293..a38e4f67 100644 --- a/rsconcept/backend/apps/rsform/models/LibraryTemplate.py +++ b/rsconcept/backend/apps/rsform/models/LibraryTemplate.py @@ -6,7 +6,7 @@ class LibraryTemplate(Model): ''' Template for library items and constituents. ''' lib_source: ForeignKey = ForeignKey( verbose_name='Источник', - to='rsform.LibraryItem', + to='rsform.RSForm', on_delete=CASCADE, null=True ) diff --git a/rsconcept/backend/apps/rsform/models/api_RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py similarity index 95% rename from rsconcept/backend/apps/rsform/models/api_RSForm.py rename to rsconcept/backend/apps/rsform/models/RSForm.py index 9790f6b0..959d6dd2 100644 --- a/rsconcept/backend/apps/rsform/models/api_RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -5,7 +5,7 @@ from typing import 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 django.db.models import Manager, QuerySet from shared import messages as msg @@ -28,21 +28,29 @@ from .Version import Version _INSERT_LAST: int = -1 -class RSForm: +class RSForm(LibraryItem): ''' RSForm is math form of conceptual schema. ''' - def __init__(self, item: LibraryItem): - if item.item_type != LibraryItemType.RSFORM: - raise ValueError(msg.libraryTypeUnexpected()) - self.item = item + class Meta: + ''' Model metadata. ''' + proxy = True - @staticmethod - def create(**kwargs) -> 'RSForm': - return RSForm(LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)) + class InternalManager(Manager): + ''' Object manager. ''' + + def get_queryset(self) -> QuerySet: + return super().get_queryset().filter(item_type=LibraryItemType.RSFORM) + + def create(self, **kwargs): + kwargs.update({'item_type': LibraryItemType.RSFORM}) + return super().create(**kwargs) + + # Legit overriding object manager + objects = InternalManager() # type: ignore[misc] def constituents(self) -> QuerySet[Constituenta]: ''' Get QuerySet containing all constituents of current RSForm. ''' - return Constituenta.objects.filter(schema=self.item) + return Constituenta.objects.filter(schema=self.pk) def resolver(self) -> Resolver: ''' Create resolver for text references based on schema terms. ''' @@ -98,7 +106,7 @@ class RSForm: ''' Get maximum alias index for specific CstType. ''' result: int = 0 items = Constituenta.objects \ - .filter(schema=self.item, cst_type=cst_type) \ + .filter(schema=self, cst_type=cst_type) \ .order_by('-alias') \ .values_list('alias', flat=True) for alias in items: @@ -150,13 +158,13 @@ class RSForm: cst_type = guess_type(alias) self._shift_positions(position, 1) result = Constituenta.objects.create( - schema=self.item, + schema=self, order=position, alias=alias, cst_type=cst_type, **kwargs ) - self.item.save() + self.save() result.refresh_from_db() return result @@ -183,13 +191,13 @@ class RSForm: result = deepcopy(items) for cst in result: cst.pk = None - cst.schema = self.item + cst.schema = self cst.order = position cst.alias = mapping[cst.alias] cst.apply_mapping(mapping) cst.save() position = position + 1 - self.item.save() + self.save() return result @transaction.atomic @@ -213,7 +221,7 @@ class RSForm: count_moved += 1 update_list.append(cst) Constituenta.objects.bulk_update(update_list, ['order']) - self.item.save() + self.save() @transaction.atomic def delete_cst(self, listCst): @@ -222,7 +230,7 @@ class RSForm: cst.delete() self._reset_order() self.resolve_all_text() - self.item.save() + self.save() @transaction.atomic def substitute( @@ -296,7 +304,7 @@ class RSForm: def create_version(self, version: str, description: str, data) -> Version: ''' Creates version for current state. ''' return Version.objects.create( - item=self.item, + item=self, version=version, description=description, data=data @@ -322,7 +330,7 @@ class RSForm: prefix = get_type_prefix(cst_type) for text in expressions: new_item = Constituenta.objects.create( - schema=self.item, + schema=self, order=position, alias=f'{prefix}{free_index}', definition_formal=text, @@ -332,7 +340,7 @@ class RSForm: free_index = free_index + 1 position = position + 1 - self.item.save() + self.save() return result def _shift_positions(self, start: int, shift: int): @@ -341,7 +349,7 @@ class RSForm: update_list = \ Constituenta.objects \ .only('id', 'order', 'schema') \ - .filter(schema=self.item, order__gte=start) + .filter(schema=self.pk, order__gte=start) for cst in update_list: cst.order += shift Constituenta.objects.bulk_update(update_list, ['order']) diff --git a/rsconcept/backend/apps/rsform/models/__init__.py b/rsconcept/backend/apps/rsform/models/__init__.py index 3ea203d6..d6a5bc27 100644 --- a/rsconcept/backend/apps/rsform/models/__init__.py +++ b/rsconcept/backend/apps/rsform/models/__init__.py @@ -1,6 +1,6 @@ ''' Django: Models. ''' -from .api_RSForm import RSForm +from .RSForm import RSForm from .Constituenta import Constituenta, CstType, _empty_forms from .Editor import Editor from .LibraryItem import ( diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index b181fa74..68d9e1a1 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -109,22 +109,21 @@ class CstSerializer(serializers.ModelSerializer): def update(self, instance: Constituenta, validated_data) -> Constituenta: data = validated_data # Note: use alias for better code readability - schema = RSForm(instance.schema) definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None term: Optional[str] = data['term_raw'] if 'term_raw' in data else None term_changed = 'term_forms' in data if definition is not None and definition != instance.definition_raw: - data['definition_resolved'] = schema.resolver().resolve(definition) + data['definition_resolved'] = instance.schema.resolver().resolve(definition) if term is not None and term != instance.term_raw: - data['term_resolved'] = schema.resolver().resolve(term) + data['term_resolved'] = instance.schema.resolver().resolve(term) if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data: data['term_forms'] = [] term_changed = data['term_resolved'] != instance.term_resolved result: Constituenta = super().update(instance, data) if term_changed: - schema.on_term_change([result.id]) + instance.schema.on_term_change([result.id]) result.refresh_from_db() - schema.item.save() + instance.schema.save() return result @@ -170,17 +169,16 @@ class RSFormSerializer(serializers.ModelSerializer): model = LibraryItem fields = '__all__' - def to_representation(self, instance: LibraryItem) -> dict: + def to_representation(self, instance: RSForm) -> dict: result = LibraryItemDetailsSerializer(instance).data - schema = RSForm(instance) result['items'] = [] - for cst in schema.constituents().order_by('order'): + for cst in instance.constituents().order_by('order'): result['items'].append(CstSerializer(cst).data) return result def to_versioned_data(self) -> dict: ''' Create serializable version representation without redundant data. ''' - result = self.to_representation(cast(LibraryItem, self.instance)) + result = self.to_representation(cast(RSForm, self.instance)) del result['versions'] del result['subscribers'] del result['editors'] @@ -197,14 +195,14 @@ class RSFormSerializer(serializers.ModelSerializer): def from_versioned_data(self, version: int, data: dict) -> dict: ''' Load data from version. ''' - result = self.to_representation(cast(LibraryItem, self.instance)) + result = self.to_representation(cast(RSForm, self.instance)) 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)) + schema = cast(RSForm, self.instance) items: list[dict] = data['items'] ids: list[int] = [item['id'] for item in items] processed: list[int] = [] @@ -258,13 +256,13 @@ class RSFormParseSerializer(serializers.ModelSerializer): model = LibraryItem fields = '__all__' - def to_representation(self, instance: LibraryItem): + def to_representation(self, instance: RSForm): result = RSFormSerializer(instance).data return self._parse_data(result) def from_versioned_data(self, version: int, data: dict) -> dict: ''' Load data from version and parse. ''' - item = cast(LibraryItem, self.instance) + item = cast(RSForm, self.instance) result = RSFormSerializer(item).from_versioned_data(version, data) return self._parse_data(result) @@ -283,7 +281,7 @@ class CstTargetSerializer(serializers.Serializer): target = PKField(many=False, queryset=Constituenta.objects.all()) def validate(self, attrs): - schema = cast(LibraryItem, self.context['schema']) + schema = cast(RSForm, self.context['schema']) cst = cast(Constituenta, attrs['target']) if schema and cst.schema != schema: raise serializers.ValidationError({ @@ -315,7 +313,7 @@ class CstRenameSerializer(serializers.Serializer): def validate(self, attrs): attrs = super().validate(attrs) - schema = cast(LibraryItem, self.context['schema']) + schema = cast(RSForm, self.context['schema']) cst = cast(Constituenta, attrs['target']) if cst.schema != schema: raise serializers.ValidationError({ @@ -326,7 +324,7 @@ class CstRenameSerializer(serializers.Serializer): raise serializers.ValidationError({ 'alias': msg.renameTrivial(new_alias) }) - if RSForm(schema).constituents().filter(alias=new_alias).exists(): + if schema.constituents().filter(alias=new_alias).exists(): raise serializers.ValidationError({ 'alias': msg.aliasTaken(new_alias) }) @@ -338,7 +336,7 @@ class CstListSerializer(serializers.Serializer): items = PKField(many=True, queryset=Constituenta.objects.all()) def validate(self, attrs): - schema = cast(LibraryItem, self.context['schema']) + schema = cast(RSForm, self.context['schema']) if not schema: return attrs @@ -370,7 +368,7 @@ class CstSubstituteSerializer(serializers.Serializer): ) def validate(self, attrs): - schema = cast(LibraryItem, self.context['schema']) + schema = cast(RSForm, self.context['schema']) deleted = set() for item in attrs['substitutions']: original_cst = cast(Constituenta, item['original']) @@ -397,8 +395,8 @@ class CstSubstituteSerializer(serializers.Serializer): class InlineSynthesisSerializer(serializers.Serializer): ''' Serializer: Inline synthesis operation input. ''' - receiver = PKField(many=False, queryset=LibraryItem.objects.all()) - source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore + receiver = PKField(many=False, queryset=RSForm.objects.all()) + source = PKField(many=False, queryset=RSForm.objects.all()) # type: ignore items = PKField(many=True, queryset=Constituenta.objects.all()) substitutions = serializers.ListField( child=CstSubstituteSerializerBase() @@ -406,8 +404,8 @@ class InlineSynthesisSerializer(serializers.Serializer): def validate(self, attrs): user = cast(User, self.context['user']) - schema_in = cast(LibraryItem, attrs['source']) - schema_out = cast(LibraryItem, attrs['receiver']) + schema_in = cast(RSForm, attrs['source']) + schema_out = cast(RSForm, attrs['receiver']) if user.is_anonymous or (schema_out.owner != user and not user.is_staff): raise PermissionDenied({ 'message': msg.schemaNotOwned(), diff --git a/rsconcept/backend/apps/rsform/serializers/io_files.py b/rsconcept/backend/apps/rsform/serializers/io_files.py index afc59108..45f05578 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_files.py +++ b/rsconcept/backend/apps/rsform/serializers/io_files.py @@ -39,9 +39,9 @@ class RSFormTRSSerializer(serializers.Serializer): def _prepare_json_rsform(schema: RSForm) -> dict: return { 'type': _TRS_TYPE, - 'title': schema.item.title, - 'alias': schema.item.alias, - 'comment': schema.item.comment, + 'title': schema.title, + 'alias': schema.alias, + 'comment': schema.comment, 'items': [], 'claimed': False, 'selection': [], @@ -125,7 +125,7 @@ class RSFormTRSSerializer(serializers.Serializer): result['comment'] = data.get('comment', '') if 'id' in data: result['id'] = data['id'] - self.instance = RSForm(LibraryItem.objects.get(pk=result['id'])) + self.instance = RSForm.objects.get(pk=result['id']) return result def validate(self, attrs: dict): @@ -139,7 +139,7 @@ class RSFormTRSSerializer(serializers.Serializer): @transaction.atomic def create(self, validated_data: dict) -> RSForm: - self.instance: RSForm = RSForm.create( + self.instance: RSForm = RSForm.objects.create( owner=validated_data.get('owner', None), alias=validated_data['alias'], title=validated_data['title'], @@ -149,12 +149,12 @@ class RSFormTRSSerializer(serializers.Serializer): access_policy=validated_data['access_policy'], location=validated_data['location'] ) - self.instance.item.save() + self.instance.save() order = 1 for cst_data in validated_data['items']: cst = Constituenta( alias=cst_data['alias'], - schema=self.instance.item, + schema=self.instance, order=order, cst_type=cst_data['cstType'], ) @@ -167,11 +167,11 @@ class RSFormTRSSerializer(serializers.Serializer): @transaction.atomic def update(self, instance: RSForm, validated_data) -> RSForm: if 'alias' in validated_data: - instance.item.alias = validated_data['alias'] + instance.alias = validated_data['alias'] if 'title' in validated_data: - instance.item.title = validated_data['title'] + instance.title = validated_data['title'] if 'comment' in validated_data: - instance.item.comment = validated_data['comment'] + instance.comment = validated_data['comment'] order = 1 prev_constituents = instance.constituents() @@ -188,7 +188,7 @@ class RSFormTRSSerializer(serializers.Serializer): else: cst = Constituenta( alias=cst_data['alias'], - schema=instance.item, + schema=instance, order=order, cst_type=cst_data['cstType'], ) @@ -202,7 +202,7 @@ class RSFormTRSSerializer(serializers.Serializer): prev_cst.delete() instance.resolve_all_text() - instance.item.save() + instance.save() return instance @staticmethod diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py index ad0ca54b..83d56973 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py @@ -3,15 +3,15 @@ from django.db.utils import IntegrityError from django.forms import ValidationError from django.test import TestCase -from apps.rsform.models import Constituenta, CstType, LibraryItem, LibraryItemType +from apps.rsform.models import Constituenta, CstType, LibraryItemType, RSForm class TestConstituenta(TestCase): ''' Testing Constituenta model. ''' def setUp(self): - self.schema1 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test1') - self.schema2 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test2') + self.schema1 = RSForm.objects.create(title='Test1') + self.schema2 = RSForm.objects.create(title='Test2') def test_str(self): diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py index 1f25ad4d..1e64e645 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Editor.py @@ -1,7 +1,7 @@ ''' Testing models: Editor. ''' from django.test import TestCase -from apps.rsform.models import Editor, LibraryItem, LibraryItemType, User +from apps.rsform.models import Editor, LibraryItemType, RSForm, User class TestEditor(TestCase): @@ -10,8 +10,7 @@ class TestEditor(TestCase): def setUp(self): self.user1 = User.objects.create(username='User1') self.user2 = User.objects.create(username='User2') - self.item = LibraryItem.objects.create( - item_type=LibraryItemType.RSFORM, + self.item = RSForm.objects.create( title='Test', alias='КС1', owner=self.user1 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 195bb080..9c47a3ac 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py @@ -11,49 +11,49 @@ class TestRSForm(TestCase): def setUp(self): self.user1 = User.objects.create(username='User1') self.user2 = User.objects.create(username='User2') - self.schema = RSForm.create(title='Test') + self.schema = RSForm.objects.create(title='Test') self.assertNotEqual(self.user1, self.user2) def test_constituents(self): - schema1 = RSForm.create(title='Test1') - schema2 = RSForm.create(title='Test2') + schema1 = RSForm.objects.create(title='Test1') + schema2 = RSForm.objects.create(title='Test2') self.assertFalse(schema1.constituents().exists()) self.assertFalse(schema2.constituents().exists()) - Constituenta.objects.create(alias='X1', schema=schema1.item, order=1) - Constituenta.objects.create(alias='X2', schema=schema1.item, order=2) + Constituenta.objects.create(alias='X1', schema=schema1, order=1) + Constituenta.objects.create(alias='X2', schema=schema1, order=2) self.assertTrue(schema1.constituents().exists()) self.assertFalse(schema2.constituents().exists()) self.assertEqual(schema1.constituents().count(), 2) def test_get_max_index(self): - schema1 = RSForm.create(title='Test1') - Constituenta.objects.create(alias='X1', schema=schema1.item, order=1) - Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.item, order=2) + schema1 = RSForm.objects.create(title='Test1') + Constituenta.objects.create(alias='X1', schema=schema1, order=1) + Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1, order=2) self.assertEqual(schema1.get_max_index(CstType.BASE), 1) self.assertEqual(schema1.get_max_index(CstType.TERM), 2) self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0) def test_insert_at(self): - schema = RSForm.create(title='Test') + schema = RSForm.objects.create(title='Test') x1 = schema.insert_new('X1') self.assertEqual(x1.order, 1) - self.assertEqual(x1.schema, schema.item) + self.assertEqual(x1.schema, schema) x2 = schema.insert_new('X2', position=1) x1.refresh_from_db() self.assertEqual(x2.order, 1) - self.assertEqual(x2.schema, schema.item) + self.assertEqual(x2.schema, schema) self.assertEqual(x1.order, 2) x3 = schema.insert_new('X3', position=4) x2.refresh_from_db() x1.refresh_from_db() self.assertEqual(x3.order, 3) - self.assertEqual(x3.schema, schema.item) + self.assertEqual(x3.schema, schema) self.assertEqual(x2.order, 1) self.assertEqual(x1.order, 2) @@ -62,7 +62,7 @@ class TestRSForm(TestCase): x2.refresh_from_db() x1.refresh_from_db() self.assertEqual(x4.order, 3) - self.assertEqual(x4.schema, schema.item) + self.assertEqual(x4.schema, schema) self.assertEqual(x3.order, 4) self.assertEqual(x2.order, 1) self.assertEqual(x1.order, 2) @@ -94,11 +94,11 @@ class TestRSForm(TestCase): def test_insert_last(self): x1 = self.schema.insert_new('X1') self.assertEqual(x1.order, 1) - self.assertEqual(x1.schema, self.schema.item) + self.assertEqual(x1.schema, self.schema) x2 = self.schema.insert_new('X2') self.assertEqual(x2.order, 2) - self.assertEqual(x2.schema, self.schema.item) + self.assertEqual(x2.schema, self.schema) self.assertEqual(x1.order, 1) def test_create_cst(self): diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py b/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py index b11d950e..a166a8af 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py @@ -8,12 +8,12 @@ class TestConstituentaAPI(EndpointTester): def setUp(self): super().setUp() - self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user) - self.rsform_unowned = RSForm.create(title='Test2', alias='T2') + self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) + self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2') self.cst1 = Constituenta.objects.create( alias='X1', cst_type=CstType.BASE, - schema=self.rsform_owned.item, + schema=self.rsform_owned, order=1, convention='Test', term_raw='Test1', @@ -22,7 +22,7 @@ class TestConstituentaAPI(EndpointTester): self.cst2 = Constituenta.objects.create( alias='X2', cst_type=CstType.BASE, - schema=self.rsform_unowned.item, + schema=self.rsform_unowned, order=1, convention='Test1', term_raw='Test2', @@ -30,7 +30,7 @@ class TestConstituentaAPI(EndpointTester): ) self.cst3 = Constituenta.objects.create( alias='X3', - schema=self.rsform_owned.item, + schema=self.rsform_owned, order=2, term_raw='Test3', term_resolved='Test3', diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_library.py b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py index d83700fc..1626b044 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py @@ -20,20 +20,16 @@ class TestLibraryViewset(EndpointTester): def setUp(self): super().setUp() - self.owned = LibraryItem.objects.create( - item_type=LibraryItemType.RSFORM, + self.owned = RSForm.objects.create( title='Test', alias='T1', owner=self.user ) - self.schema = RSForm(self.owned) - self.unowned = LibraryItem.objects.create( - item_type=LibraryItemType.RSFORM, + self.unowned = RSForm.objects.create( title='Test2', alias='T2' ) - self.common = LibraryItem.objects.create( - item_type=LibraryItemType.RSFORM, + self.common = RSForm.objects.create( title='Test3', alias='T3', location=LocationHead.COMMON @@ -363,12 +359,12 @@ class TestLibraryViewset(EndpointTester): @decl_endpoint('/api/library/{item}/clone', method='post') def test_clone_rsform(self): - x12 = self.schema.insert_new( + x12 = self.owned.insert_new( alias='X12', term_raw='человек', term_resolved='человек' ) - d2 = self.schema.insert_new( + d2 = self.owned.insert_new( alias='D2', term_raw='@{X12|plur}', term_resolved='люди' diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py b/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py index e74d9422..38b7e003 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py @@ -10,16 +10,16 @@ class TestInlineSynthesis(EndpointTester): @decl_endpoint('/api/operations/inline-synthesis', method='patch') def setUp(self): super().setUp() - self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user) - self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user) - self.unowned = RSForm.create(title='Test3', alias='T3') + self.schema1 = RSForm.objects.create(title='Test1', alias='T1', owner=self.user) + self.schema2 = RSForm.objects.create(title='Test2', alias='T2', owner=self.user) + self.unowned = RSForm.objects.create(title='Test3', alias='T3') def test_inline_synthesis_inputs(self): invalid_id = 1338 data = { - 'receiver': self.unowned.item.pk, - 'source': self.schema1.item.pk, + 'receiver': self.unowned.pk, + 'source': self.schema1.pk, 'items': [], 'substitutions': [] } @@ -28,11 +28,11 @@ class TestInlineSynthesis(EndpointTester): data['receiver'] = invalid_id self.executeBadData(data=data) - data['receiver'] = self.schema1.item.pk + data['receiver'] = self.schema1.pk data['source'] = invalid_id self.executeBadData(data=data) - data['source'] = self.schema1.item.pk + data['source'] = self.schema1.pk self.executeOK(data=data) data['items'] = [invalid_id] @@ -51,8 +51,8 @@ class TestInlineSynthesis(EndpointTester): ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items data = { - 'receiver': self.schema1.item.pk, - 'source': self.schema2.item.pk, + 'receiver': self.schema1.pk, + 'source': self.schema2.pk, 'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk], 'substitutions': [ { diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py index aa4ee1bc..bd214c8b 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -24,12 +24,12 @@ class TestRSFormViewset(EndpointTester): def setUp(self): super().setUp() - self.owned = RSForm.create(title='Test', alias='T1', owner=self.user) - self.owned_id = self.owned.item.pk - self.unowned = RSForm.create(title='Test2', alias='T2') - self.unowned_id = self.unowned.item.pk - self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) - self.private_id = self.private.item.pk + self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) + self.owned_id = self.owned.pk + self.unowned = RSForm.objects.create(title='Test2', alias='T2') + self.unowned_id = self.unowned.pk + self.private = RSForm.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) + self.private_id = self.private.pk @decl_endpoint('/api/rsforms/create-detailed', method='post') @@ -63,19 +63,19 @@ class TestRSFormViewset(EndpointTester): ) response = self.executeOK() self.assertFalse(response_contains(response, non_schema)) - self.assertTrue(response_contains(response, self.unowned.item)) - self.assertTrue(response_contains(response, self.owned.item)) + self.assertTrue(response_contains(response, self.unowned)) + self.assertTrue(response_contains(response, self.owned)) @decl_endpoint('/api/rsforms/{item}/contents', method='get') def test_contents(self): response = self.executeOK(item=self.owned_id) - self.assertEqual(response.data['owner'], self.owned.item.owner.pk) - self.assertEqual(response.data['title'], self.owned.item.title) - self.assertEqual(response.data['alias'], self.owned.item.alias) - self.assertEqual(response.data['location'], self.owned.item.location) - self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) - self.assertEqual(response.data['visible'], self.owned.item.visible) + self.assertEqual(response.data['owner'], self.owned.owner.pk) + self.assertEqual(response.data['title'], self.owned.title) + self.assertEqual(response.data['alias'], self.owned.alias) + self.assertEqual(response.data['location'], self.owned.location) + self.assertEqual(response.data['access_policy'], self.owned.access_policy) + self.assertEqual(response.data['visible'], self.owned.visible) @decl_endpoint('/api/rsforms/{item}/details', method='get') @@ -92,12 +92,12 @@ class TestRSFormViewset(EndpointTester): ) response = self.executeOK(item=self.owned_id) - self.assertEqual(response.data['owner'], self.owned.item.owner.pk) - self.assertEqual(response.data['title'], self.owned.item.title) - self.assertEqual(response.data['alias'], self.owned.item.alias) - self.assertEqual(response.data['location'], self.owned.item.location) - self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) - self.assertEqual(response.data['visible'], self.owned.item.visible) + self.assertEqual(response.data['owner'], self.owned.owner.pk) + self.assertEqual(response.data['title'], self.owned.title) + self.assertEqual(response.data['alias'], self.owned.alias) + self.assertEqual(response.data['location'], self.owned.location) + self.assertEqual(response.data['access_policy'], self.owned.access_policy) + self.assertEqual(response.data['visible'], self.owned.visible) self.assertEqual(len(response.data['items']), 2) self.assertEqual(response.data['items'][0]['id'], x1.pk) @@ -176,9 +176,9 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/export-trs', method='get') def test_export_trs(self): - schema = RSForm.create(title='Test') + schema = RSForm.objects.create(title='Test') schema.insert_new('X1') - response = self.executeOK(item=schema.item.pk) + response = self.executeOK(item=schema.pk) self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') with io.BytesIO(response.content) as stream: with ZipFile(stream, 'r') as zipped_file: @@ -387,7 +387,7 @@ class TestRSFormViewset(EndpointTester): data = {'items': [x1.pk]} response = self.executeOK(data=data) x2.refresh_from_db() - self.owned.item.refresh_from_db() + self.owned.refresh_from_db() self.assertEqual(len(response.data['items']), 1) self.assertEqual(self.owned.constituents().count(), 1) self.assertEqual(x2.alias, 'X2') @@ -449,16 +449,16 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/load-trs', method='patch') def test_load_trs(self): self.set_params(item=self.owned_id) - self.owned.item.title = 'Test11' - self.owned.item.save() + self.owned.title = 'Test11' + self.owned.save() x1 = self.owned.insert_new('X1') work_dir = os.path.dirname(os.path.abspath(__file__)) with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: data = {'file': file, 'load_metadata': False} response = self.client.patch(self.endpoint, data=data, format='multipart') - self.owned.item.refresh_from_db() + self.owned.refresh_from_db() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.owned.item.title, 'Test11') + self.assertEqual(self.owned.title, 'Test11') self.assertEqual(len(response.data['items']), 25) self.assertEqual(self.owned.constituents().count(), 25) self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists()) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py b/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py index cc8433cd..5ea85c49 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py @@ -15,10 +15,9 @@ class TestVersionViews(EndpointTester): def setUp(self): super().setUp() - self.owned = RSForm.create(title='Test', alias='T1', owner=self.user).item - self.schema = RSForm(self.owned) - self.unowned = RSForm.create(title='Test2', alias='T2').item - self.x1 = self.schema.insert_new( + self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) + self.unowned = RSForm.objects.create(title='Test2', alias='T2') + self.x1 = self.owned.insert_new( alias='X1', convention='testStart' ) @@ -135,14 +134,14 @@ class TestVersionViews(EndpointTester): @decl_endpoint('/api/versions/{version}/restore', method='patch') def test_restore_version(self): x1 = self.x1 - x2 = self.schema.insert_new('X2') - d1 = self.schema.insert_new('D1', term_raw='TestTerm') + x2 = self.owned.insert_new('X2') + d1 = self.owned.insert_new('D1', term_raw='TestTerm') data = {'version': '1.0.0', 'description': 'test'} version_id = self._create_version(data=data) invalid_id = version_id + 1337 d1.delete() - x3 = self.schema.insert_new('X3') + x3 = self.owned.insert_new('X3') x1.order = x3.order x1.convention = 'Test2' x1.term_raw = 'Test' diff --git a/rsconcept/backend/apps/rsform/views/library.py b/rsconcept/backend/apps/rsform/views/library.py index 4748a7d8..ffb8bb62 100644 --- a/rsconcept/backend/apps/rsform/views/library.py +++ b/rsconcept/backend/apps/rsform/views/library.py @@ -85,7 +85,11 @@ class LibraryViewSet(viewsets.ModelViewSet): serializer = s.LibraryItemCloneSerializer(data=request.data) serializer.is_valid(raise_exception=True) item = self._get_item() - clone = deepcopy(item) + if item.item_type != m.LibraryItemType.RSFORM: + return Response(status=c.HTTP_400_BAD_REQUEST) + + schema = m.RSForm.objects.get(pk=item.pk) + clone = deepcopy(schema) clone.pk = None clone.owner = self.request.user clone.title = serializer.validated_data['title'] @@ -98,18 +102,16 @@ class LibraryViewSet(viewsets.ModelViewSet): with transaction.atomic(): clone.save() - if clone.item_type == m.LibraryItemType.RSFORM: - need_filter = 'items' in request.data - for cst in m.RSForm(item).constituents(): - if not need_filter or cst.pk in request.data['items']: - cst.pk = None - cst.schema = clone - cst.save() - return Response( - status=c.HTTP_201_CREATED, - data=s.RSFormParseSerializer(clone).data - ) - return Response(status=c.HTTP_400_BAD_REQUEST) + need_filter = 'items' in request.data + for cst in schema.constituents(): + if not need_filter or cst.pk in request.data['items']: + cst.pk = None + cst.schema = clone + cst.save() + return Response( + status=c.HTTP_201_CREATED, + data=s.RSFormParseSerializer(clone).data + ) @extend_schema( summary='subscribe to item', diff --git a/rsconcept/backend/apps/rsform/views/operations.py b/rsconcept/backend/apps/rsform/views/operations.py index 3dca6751..51e441e8 100644 --- a/rsconcept/backend/apps/rsform/views/operations.py +++ b/rsconcept/backend/apps/rsform/views/operations.py @@ -27,7 +27,7 @@ def inline_synthesis(request: Request): ) serializer.is_valid(raise_exception=True) - schema = m.RSForm(serializer.validated_data['receiver']) + schema = cast(m.RSForm, serializer.validated_data['receiver']) items = cast(list[m.Constituenta], serializer.validated_data['items']) with transaction.atomic(): @@ -46,5 +46,5 @@ def inline_synthesis(request: Request): return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(schema.item).data + data=s.RSFormParseSerializer(schema).data ) diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 5c3a4e9a..ff96708e 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -25,11 +25,11 @@ from .. import utils @extend_schema_view() class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): ''' Endpoint: RSForm operations. ''' - queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM) + queryset = m.RSForm.objects.all() serializer_class = s.LibraryItemSerializer def _get_schema(self) -> m.RSForm: - return m.RSForm(cast(m.LibraryItem, self.get_object())) + return cast(m.RSForm, self.get_object()) def get_permissions(self): ''' Determine permission class. ''' @@ -81,12 +81,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr insert_after = None new_cst = schema.create_cst(data, insert_after) - schema.item.refresh_from_db() + schema.refresh_from_db() response = Response( status=c.HTTP_201_CREATED, data={ 'new_cst': s.CstSerializer(new_cst).data, - 'schema': s.RSFormParseSerializer(schema.item).data + 'schema': s.RSFormParseSerializer(schema).data } ) response['Location'] = new_cst.get_absolute_url() @@ -108,11 +108,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr ''' Produce a term for every element of the target constituenta typification. ''' schema = self._get_schema() - serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema.item}) + serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) cst = cast(m.Constituenta, serializer.validated_data['target']) - schema_details = s.RSFormParseSerializer(schema.item).data['items'] + schema_details = s.RSFormParseSerializer(schema).data['items'] cst_parse = next(item for item in schema_details if item['id'] == cst.id)['parse'] if not cst_parse['typification']: return Response( @@ -125,7 +125,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr status=c.HTTP_200_OK, data={ 'cst_list': result, - 'schema': s.RSFormParseSerializer(schema.item).data + 'schema': s.RSFormParseSerializer(schema).data } ) @@ -144,7 +144,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr def cst_rename(self, request: Request, pk): ''' Rename constituenta possibly changing type. ''' schema = self._get_schema() - serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema.item}) + serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) cst = cast(m.Constituenta, serializer.validated_data['target']) @@ -156,14 +156,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr with transaction.atomic(): cst.save() schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False) - schema.item.refresh_from_db() + schema.refresh_from_db() cst.refresh_from_db() return Response( status=c.HTTP_200_OK, data={ 'new_cst': s.CstSerializer(cst).data, - 'schema': s.RSFormParseSerializer(schema.item).data + 'schema': s.RSFormParseSerializer(schema).data } ) @@ -184,7 +184,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema = self._get_schema() serializer = s.CstSubstituteSerializer( data=request.data, - context={'schema': schema.item} + context={'schema': schema} ) serializer.is_valid(raise_exception=True) @@ -193,10 +193,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr original = cast(m.Constituenta, substitution['original']) replacement = cast(m.Constituenta, substitution['substitution']) schema.substitute(original, replacement, substitution['transfer_term']) - schema.item.refresh_from_db() + schema.refresh_from_db() return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(schema.item).data + data=s.RSFormParseSerializer(schema).data ) @extend_schema( @@ -216,14 +216,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema = self._get_schema() serializer = s.CstListSerializer( data=request.data, - context={'schema': schema.item} + context={'schema': schema} ) serializer.is_valid(raise_exception=True) schema.delete_cst(serializer.validated_data['items']) - schema.item.refresh_from_db() + schema.refresh_from_db() return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(schema.item).data + data=s.RSFormParseSerializer(schema).data ) @extend_schema( @@ -243,7 +243,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema = self._get_schema() serializer = s.CstMoveSerializer( data=request.data, - context={'schema': schema.item} + context={'schema': schema} ) serializer.is_valid(raise_exception=True) schema.move_cst( @@ -252,7 +252,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr ) return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(schema.item).data + data=s.RSFormParseSerializer(schema).data ) @extend_schema( @@ -272,7 +272,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema.reset_aliases() return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(schema.item).data + data=s.RSFormParseSerializer(schema).data ) @extend_schema( @@ -292,7 +292,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema.restore_order() return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(schema.item).data + data=s.RSFormParseSerializer(schema).data ) @extend_schema( @@ -314,7 +314,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema = self._get_schema() load_metadata = input_serializer.validated_data['load_metadata'] data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) - data['id'] = schema.item.pk + data['id'] = schema.pk serializer = s.RSFormTRSSerializer( data=data, @@ -324,7 +324,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr result = serializer.save() return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(result.item).data + data=s.RSFormParseSerializer(result).data ) @extend_schema( @@ -357,7 +357,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr @action(detail=True, methods=['get'], url_path='details') def details(self, request: Request, pk): ''' Endpoint: Detailed schema view including statuses and parse. ''' - serializer = s.RSFormParseSerializer(cast(m.LibraryItem, self.get_object())) + serializer = s.RSFormParseSerializer(self._get_schema()) return Response( status=c.HTTP_200_OK, data=serializer.data @@ -421,7 +421,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr ''' Endpoint: Download Exteor compatible file. ''' data = s.RSFormTRSSerializer(self._get_schema()).data file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) - filename = utils.filename_for_schema(self._get_schema().item.alias) + filename = utils.filename_for_schema(self._get_schema().alias) response = HttpResponse(file, content_type='application/zip') response['Content-Disposition'] = f'attachment; filename={filename}' return response @@ -451,7 +451,7 @@ class TrsImportView(views.APIView): ) serializer.is_valid(raise_exception=True) schema = serializer.save() - result = s.LibraryItemSerializer(schema.item) + result = s.LibraryItemSerializer(schema) return Response( status=c.HTTP_201_CREATED, data=result.data @@ -483,7 +483,7 @@ def create_rsform(request: Request): serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer_rsform.is_valid(raise_exception=True) schema = serializer_rsform.save() - result = s.LibraryItemSerializer(schema.item) + result = s.LibraryItemSerializer(schema) return Response( status=c.HTTP_201_CREATED, data=result.data diff --git a/rsconcept/backend/apps/rsform/views/versions.py b/rsconcept/backend/apps/rsform/views/versions.py index 318e4da5..2cdefdde 100644 --- a/rsconcept/backend/apps/rsform/views/versions.py +++ b/rsconcept/backend/apps/rsform/views/versions.py @@ -42,10 +42,11 @@ class VersionViewset( ''' Restore version data into current item. ''' version = cast(m.Version, self.get_object()) item = cast(m.LibraryItem, version.item) - s.RSFormSerializer(item).restore_from_version(version.data) + schema = m.RSForm.objects.get(pk=item.pk) + s.RSFormSerializer(schema).restore_from_version(version.data) return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(item).data + data=s.RSFormParseSerializer(schema).data ) @@ -65,7 +66,7 @@ class VersionViewset( def create_version(request: Request, pk_item: int): ''' Endpoint: Create new version for RSForm copying current content. ''' try: - item = m.LibraryItem.objects.get(pk=pk_item) + item = m.RSForm.objects.get(pk=pk_item) except m.LibraryItem.DoesNotExist: return Response(status=c.HTTP_404_NOT_FOUND) creator = request.user @@ -75,7 +76,7 @@ def create_version(request: Request, pk_item: int): version_input = s.VersionCreateSerializer(data=request.data) version_input.is_valid(raise_exception=True) data = s.RSFormSerializer(item).to_versioned_data() - result = m.RSForm(item).create_version( + result = item.create_version( version=version_input.validated_data['version'], description=version_input.validated_data['description'], data=data @@ -102,8 +103,8 @@ def create_version(request: Request, pk_item: int): def retrieve_version(request: Request, pk_item: int, pk_version: int): ''' Endpoint: Retrieve version for RSForm. ''' try: - item = m.LibraryItem.objects.get(pk=pk_item) - except m.LibraryItem.DoesNotExist: + item = m.RSForm.objects.get(pk=pk_item) + except m.RSForm.DoesNotExist: return Response(status=c.HTTP_404_NOT_FOUND) try: version = m.Version.objects.get(pk=pk_version) @@ -135,7 +136,8 @@ def export_file(request: Request, pk: int): version = m.Version.objects.get(pk=pk) except m.Version.DoesNotExist: return Response(status=c.HTTP_404_NOT_FOUND) - data = s.RSFormTRSSerializer(m.RSForm(version.item)).from_versioned_data(version.data) + schema = m.RSForm.objects.get(pk=version.item.pk) + data = s.RSFormTRSSerializer(schema).from_versioned_data(version.data) file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) filename = utils.filename_for_schema(data['alias']) response = HttpResponse(file, content_type='application/zip') diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index 5357c542..99233be4 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -79,6 +79,8 @@ INSTALLED_APPS = [ 'drf_spectacular', 'drf_spectacular_sidecar', ] +if DEBUG: + INSTALLED_APPS.append('django_extensions') REST_FRAMEWORK = { @@ -128,7 +130,6 @@ APPEND_SLASH = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ - STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static')) STATIC_URL = 'static/' MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) @@ -156,7 +157,6 @@ WSGI_APPLICATION = 'project.wsgi.application' # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'), @@ -198,7 +198,6 @@ SPECTACULAR_SETTINGS = { # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS: list[str] = [ # NOTE: Password validators disabled # { @@ -231,6 +230,14 @@ USE_TZ = True DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# Graph model settings for visualization +# https://django-extensions.readthedocs.io/en/latest/graph_models.html +GRAPH_MODELS = { + 'all_applications': True, + 'group_models': True, +} + + LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/rsconcept/backend/requirements-dev.txt b/rsconcept/backend/requirements-dev.txt index a33d4026..6e01bde6 100644 --- a/rsconcept/backend/requirements-dev.txt +++ b/rsconcept/backend/requirements-dev.txt @@ -14,6 +14,7 @@ psycopg2-binary gunicorn djangorestframework-stubs[compatible-mypy] +django-extensions mypy pylint coverage \ No newline at end of file diff --git a/rsconcept/backend/shared/messages.py b/rsconcept/backend/shared/messages.py index dae5328e..4ef915bd 100644 --- a/rsconcept/backend/shared/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -50,10 +50,6 @@ def typificationInvalidStr(): return 'Invalid typification string' -def libraryTypeUnexpected(): - return 'Attempting to use invalid adaptor for non-RSForm item' - - def exteorFileVersionNotSupported(): return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' diff --git a/scripts/dev/GraphDB.ps1 b/scripts/dev/GraphDB.ps1 new file mode 100644 index 00000000..0282df94 --- /dev/null +++ b/scripts/dev/GraphDB.ps1 @@ -0,0 +1,16 @@ +# Generate DOT file for DB structure +$backend = Resolve-Path -Path "${PSScriptRoot}\..\..\rsconcept\backend" + +function GenerateDOT() { + Set-Location $backend + + $python = "${backend}\venv\Scripts\python.exe" + $djangoSrc = "${backend}\manage.py" + + & $python $djangoSrc graph_models -o visualizeDB.dot + + notepad.exe "${backend}\visualizeDB.dot" + Start-Process "https://dreampuf.github.io/GraphvizOnline" +} + +GenerateDOT \ No newline at end of file diff --git a/scripts/dev/RunCoverage.ps1 b/scripts/dev/RunCoverage.ps1 index 8f61f676..724670ba 100644 --- a/scripts/dev/RunCoverage.ps1 +++ b/scripts/dev/RunCoverage.ps1 @@ -2,7 +2,7 @@ $backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend" -function RunLinters() { +function RunCoverage() { BackendCoverage } @@ -20,4 +20,4 @@ function BackendCoverage() { Start-Process "$backend\htmlcov\index.html" } -RunLinters \ No newline at end of file +RunCoverage \ No newline at end of file