From da7113ce7eca52ea32de22b0a8f8b7f9dedd1c71 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:56:01 +0300 Subject: [PATCH] Refactoring: preparing backend for oss pt1 --- .vscode/settings.json | 4 +- README.md | 4 +- rsconcept/backend/apps/oss/__init__.py | 0 rsconcept/backend/apps/oss/admin.py | 14 +++ rsconcept/backend/apps/oss/apps.py | 8 ++ .../apps/oss/migrations/0001_initial.py | 69 +++++++++++ .../backend/apps/oss/migrations/__init__.py | 0 rsconcept/backend/apps/oss/models/Argument.py | 27 +++++ .../backend/apps/oss/models/Operation.py | 71 +++++++++++ .../apps/oss/models/SynthesisSubstitution.py | 36 ++++++ rsconcept/backend/apps/oss/models/__init__.py | 8 ++ rsconcept/backend/apps/oss/models/api_OSS.py | 58 +++++++++ .../backend/apps/oss/serializers/__init__.py | 11 ++ .../apps/oss/serializers/data_access.py | 97 +++++++++++++++ .../apps/oss/serializers/schema_typing.py | 10 ++ rsconcept/backend/apps/oss/urls.py | 12 ++ rsconcept/backend/apps/oss/views/__init__.py | 2 + rsconcept/backend/apps/oss/views/oss.py | 110 ++++++++++++++++++ .../0008_alter_libraryitem_item_type.py | 18 +++ .../backend/apps/rsform/models/LibraryItem.py | 13 ++- .../backend/apps/rsform/models/api_RSForm.py | 3 +- .../apps/rsform/models/api_RSLanguage.py | 3 +- .../apps/rsform/serializers/__init__.py | 1 + .../backend/apps/rsform/serializers/basics.py | 3 +- .../apps/rsform/serializers/data_access.py | 3 +- .../apps/rsform/serializers/io_files.py | 3 +- .../apps/rsform/serializers/io_pyconcept.py | 3 +- .../apps/rsform/tests/s_views/t_cctext.py | 2 +- .../rsform/tests/s_views/t_constituents.py | 3 +- .../apps/rsform/tests/s_views/t_library.py | 5 +- .../apps/rsform/tests/s_views/t_operations.py | 3 +- .../apps/rsform/tests/s_views/t_rsforms.py | 5 +- .../apps/rsform/tests/s_views/t_rslang.py | 2 +- .../apps/rsform/tests/s_views/t_versions.py | 3 +- .../backend/apps/rsform/views/constituents.py | 3 +- .../backend/apps/rsform/views/library.py | 3 +- .../backend/apps/rsform/views/rsforms.py | 5 +- .../backend/apps/rsform/views/versions.py | 3 +- rsconcept/backend/apps/users/messages.py | 14 --- rsconcept/backend/apps/users/serializers.py | 2 +- rsconcept/backend/apps/users/tests/t_views.py | 2 +- rsconcept/backend/project/settings.py | 1 + .../rsform/tests => shared}/EndpointTester.py | 0 rsconcept/backend/shared/__init__.py | 1 + .../{apps/rsform => shared}/messages.py | 16 +++ .../{apps/rsform => shared}/permissions.py | 30 +++-- .../rsform/tests => shared}/testing_utils.py | 0 47 files changed, 634 insertions(+), 60 deletions(-) create mode 100644 rsconcept/backend/apps/oss/__init__.py create mode 100644 rsconcept/backend/apps/oss/admin.py create mode 100644 rsconcept/backend/apps/oss/apps.py create mode 100644 rsconcept/backend/apps/oss/migrations/0001_initial.py create mode 100644 rsconcept/backend/apps/oss/migrations/__init__.py create mode 100644 rsconcept/backend/apps/oss/models/Argument.py create mode 100644 rsconcept/backend/apps/oss/models/Operation.py create mode 100644 rsconcept/backend/apps/oss/models/SynthesisSubstitution.py create mode 100644 rsconcept/backend/apps/oss/models/__init__.py create mode 100644 rsconcept/backend/apps/oss/models/api_OSS.py create mode 100644 rsconcept/backend/apps/oss/serializers/__init__.py create mode 100644 rsconcept/backend/apps/oss/serializers/data_access.py create mode 100644 rsconcept/backend/apps/oss/serializers/schema_typing.py create mode 100644 rsconcept/backend/apps/oss/urls.py create mode 100644 rsconcept/backend/apps/oss/views/__init__.py create mode 100644 rsconcept/backend/apps/oss/views/oss.py create mode 100644 rsconcept/backend/apps/rsform/migrations/0008_alter_libraryitem_item_type.py delete mode 100644 rsconcept/backend/apps/users/messages.py rename rsconcept/backend/{apps/rsform/tests => shared}/EndpointTester.py (100%) create mode 100644 rsconcept/backend/shared/__init__.py rename rsconcept/backend/{apps/rsform => shared}/messages.py (79%) rename rsconcept/backend/{apps/rsform => shared}/permissions.py (80%) rename rsconcept/backend/{apps/rsform/tests => shared}/testing_utils.py (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index a5c1a707..90b939b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,9 @@ "--multi-line", "3", "--project", - "apps" + "apps", + "--project", + "shared" ], "autopep8.args": [ "--max-line-length", diff --git a/README.md b/README.md index a6b96af3..51d4dd10 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ This readme file is used mostly to document project dependencies - Backticks - Svg Preview - TODO Highlight v2 + - Prettier
@@ -114,8 +115,9 @@ This readme file is used mostly to document project dependencies
   - Pylance
   - Pylint
-  - Django
   - autopep8
+  - isort
+  - Django
   - SQLite
   
diff --git a/rsconcept/backend/apps/oss/__init__.py b/rsconcept/backend/apps/oss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/oss/admin.py b/rsconcept/backend/apps/oss/admin.py new file mode 100644 index 00000000..3bf56a23 --- /dev/null +++ b/rsconcept/backend/apps/oss/admin.py @@ -0,0 +1,14 @@ +''' Admin view: OperationSchema. ''' +from django.contrib import admin + +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'] + + +admin.site.register(models.Operation, OperationAdmin) diff --git a/rsconcept/backend/apps/oss/apps.py b/rsconcept/backend/apps/oss/apps.py new file mode 100644 index 00000000..540eca26 --- /dev/null +++ b/rsconcept/backend/apps/oss/apps.py @@ -0,0 +1,8 @@ +''' Application: Operation Schema. ''' +from django.apps import AppConfig + + +class RsformConfig(AppConfig): + ''' Application config. ''' + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.oss' diff --git a/rsconcept/backend/apps/oss/migrations/0001_initial.py b/rsconcept/backend/apps/oss/migrations/0001_initial.py new file mode 100644 index 00000000..e5fc31f8 --- /dev/null +++ b/rsconcept/backend/apps/oss/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0.7 on 2024-07-17 09:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('rsform', '0008_alter_libraryitem_item_type'), + ] + + operations = [ + migrations.CreateModel( + name='Operation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('operation_type', models.CharField(choices=[ + ('input', 'Input'), ('synthesis', 'Synthesis')], default='input', max_length=10, verbose_name='Тип')), + ('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')), + ('title', models.TextField(blank=True, verbose_name='Название')), + ('comment', models.TextField(blank=True, verbose_name='Комментарий')), + ('position_x', models.FloatField(default=0, verbose_name='Положение по горизонтали')), + ('position_y', models.FloatField(default=0, verbose_name='Положение по вертикали')), + ('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='items', to='rsform.libraryitem', verbose_name='Схема синтеза')), + ('result', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, + related_name='producer', to='rsform.libraryitem', verbose_name='Связанная КС')), + ], + options={ + 'verbose_name': 'Операция', + 'verbose_name_plural': 'Операции', + }, + ), + migrations.CreateModel( + name='SynthesisSubstitution', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('transfer_term', models.BooleanField(default=False, verbose_name='Перенос термина')), + ('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + to='oss.operation', verbose_name='Операция')), + ('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='as_original', to='rsform.constituenta', verbose_name='Удаляемая конституента')), + ('substitution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='as_substitute', to='rsform.constituenta', verbose_name='Замещающая конституента')), + ], + options={ + 'verbose_name': 'Отождествление синтеза', + 'verbose_name_plural': 'Таблицы отождествлений', + }, + ), + migrations.CreateModel( + name='Argument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('argument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='descendants', to='oss.operation', verbose_name='Аргумент')), + ('operation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='arguments', to='oss.operation', verbose_name='Операция')), + ], + options={ + 'verbose_name': 'Аргумент', + 'verbose_name_plural': 'Аргументы операций', + 'unique_together': {('operation', 'argument')}, + }, + ), + ] diff --git a/rsconcept/backend/apps/oss/migrations/__init__.py b/rsconcept/backend/apps/oss/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/oss/models/Argument.py b/rsconcept/backend/apps/oss/models/Argument.py new file mode 100644 index 00000000..3282113d --- /dev/null +++ b/rsconcept/backend/apps/oss/models/Argument.py @@ -0,0 +1,27 @@ +''' Models: Operation Argument in OSS. ''' +from django.db.models import CASCADE, ForeignKey, Model + + +class Argument(Model): + ''' Operation Argument.''' + operation: ForeignKey = ForeignKey( + verbose_name='Операция', + to='oss.Operation', + on_delete=CASCADE, + related_name='arguments' + ) + argument: ForeignKey = ForeignKey( + verbose_name='Аргумент', + to='oss.Operation', + on_delete=CASCADE, + related_name='descendants' + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Аргумент' + verbose_name_plural = 'Аргументы операций' + unique_together = [['operation', 'argument']] + + def __str__(self) -> str: + return f'{self.argument.pk} -> {self.operation.pk}' diff --git a/rsconcept/backend/apps/oss/models/Operation.py b/rsconcept/backend/apps/oss/models/Operation.py new file mode 100644 index 00000000..54b16740 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/Operation.py @@ -0,0 +1,71 @@ +''' Models: Operation in OSS. ''' +from django.db.models import ( + CASCADE, + SET_NULL, + CharField, + FloatField, + ForeignKey, + Model, + TextChoices, + TextField +) + + +class OperationType(TextChoices): + ''' Type of operation. ''' + INPUT = 'input' + SYNTHESIS = 'synthesis' + + +class Operation(Model): + ''' Operational schema Unit.''' + oss: ForeignKey = ForeignKey( + verbose_name='Схема синтеза', + to='rsform.LibraryItem', + on_delete=CASCADE, + related_name='items' + ) + operation_type: CharField = CharField( + verbose_name='Тип', + max_length=10, + choices=OperationType.choices, + default=OperationType.INPUT + ) + result: ForeignKey = ForeignKey( + verbose_name='Связанная КС', + to='rsform.LibraryItem', + null=True, + on_delete=SET_NULL, + related_name='producer' + ) + + alias: CharField = CharField( + verbose_name='Шифр', + max_length=255, + blank=True + ) + title: TextField = TextField( + verbose_name='Название', + blank=True + ) + comment: TextField = TextField( + verbose_name='Комментарий', + blank=True + ) + + position_x: FloatField = FloatField( + verbose_name='Положение по горизонтали', + default=0 + ) + position_y: FloatField = FloatField( + verbose_name='Положение по вертикали', + default=0 + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Операция' + verbose_name_plural = 'Операции' + + def __str__(self) -> str: + return f'Операция {self.alias}' diff --git a/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py b/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py new file mode 100644 index 00000000..61c2da22 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/SynthesisSubstitution.py @@ -0,0 +1,36 @@ +''' Models: SynthesisSubstitution. ''' +from django.db.models import CASCADE, BooleanField, ForeignKey, Model + + +class SynthesisSubstitution(Model): + ''' Substitutions as part of Synthesis operation in OSS.''' + operation: ForeignKey = ForeignKey( + verbose_name='Операция', + to='oss.Operation', + on_delete=CASCADE + ) + + original: ForeignKey = ForeignKey( + verbose_name='Удаляемая конституента', + to='rsform.Constituenta', + on_delete=CASCADE, + related_name='as_original' + ) + substitution: ForeignKey = ForeignKey( + verbose_name='Замещающая конституента', + to='rsform.Constituenta', + on_delete=CASCADE, + related_name='as_substitute' + ) + transfer_term: BooleanField = BooleanField( + verbose_name='Перенос термина', + default=False + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Отождествление синтеза' + verbose_name_plural = 'Таблицы отождествлений' + + def __str__(self) -> str: + return f'{self.original.pk} -> {self.substitution.pk}' diff --git a/rsconcept/backend/apps/oss/models/__init__.py b/rsconcept/backend/apps/oss/models/__init__.py new file mode 100644 index 00000000..ad9d5148 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/__init__.py @@ -0,0 +1,8 @@ +''' Django: Models. ''' + +from apps.rsform.models import LibraryItem, LibraryItemType + +from .api_OSS import OperationSchema +from .Argument import Argument +from .Operation import Operation +from .SynthesisSubstitution import SynthesisSubstitution diff --git a/rsconcept/backend/apps/oss/models/api_OSS.py b/rsconcept/backend/apps/oss/models/api_OSS.py new file mode 100644 index 00000000..5172c487 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/api_OSS.py @@ -0,0 +1,58 @@ +''' Models: OSS API. ''' +from django.core.exceptions import ValidationError +from django.db import transaction +from django.db.models import QuerySet + +from apps.rsform.models import LibraryItem, LibraryItemType +from shared import messages as msg + +from .Argument import Argument +from .Operation import Operation, OperationType +from .SynthesisSubstitution import SynthesisSubstitution + + +class OperationSchema: + ''' Operations schema API. ''' + + def __init__(self, item: LibraryItem): + if item.item_type != LibraryItemType.OPERATION_SCHEMA: + raise ValueError(msg.libraryTypeUnexpected()) + self.item = item + + @staticmethod + def create(**kwargs) -> 'OperationSchema': + item = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs) + return OperationSchema(item=item) + + def operations(self) -> QuerySet[Operation]: + ''' Get QuerySet containing all operations of current OSS. ''' + return Operation.objects.filter(oss=self.item) + + def arguments(self) -> QuerySet[Argument]: + ''' Operation arguments. ''' + return Argument.objects.filter(operation__oss=self.item) + + def substitutions(self) -> QuerySet[SynthesisSubstitution]: + ''' Operation arguments. ''' + return SynthesisSubstitution.objects.filter(operation__oss=self.item) + + @transaction.atomic + def create_operation(self, operation_type: OperationType, alias: str, **kwargs) -> Operation: + ''' Insert new operation. ''' + if self.operations().filter(alias=alias).exists(): + raise ValidationError(msg.aliasTaken(alias)) + result = Operation.objects.create( + oss=self.item, + alias=alias, + operation_type=operation_type, + **kwargs + ) + self.item.save() + result.refresh_from_db() + return result + + @transaction.atomic + def delete_operation(self, operation: Operation): + ''' Delete operation. ''' + operation.delete() + self.item.save() diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py new file mode 100644 index 00000000..e844333f --- /dev/null +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -0,0 +1,11 @@ +''' REST API: Serializers. ''' + +from apps.rsform.serializers import LibraryItemSerializer + +from .data_access import ( + ArgumentSerializer, + OperationCreateSerializer, + OperationSchemaSerializer, + OperationSerializer +) +from .schema_typing import NewOperationResponse diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py new file mode 100644 index 00000000..b07135e4 --- /dev/null +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -0,0 +1,97 @@ +''' Serializers for persistent data manipulation. ''' +from typing import cast + +from rest_framework import serializers + +from apps.rsform.models import LibraryItem +from apps.rsform.serializers import LibraryItemDetailsSerializer +from shared import messages as msg + +from ..models import Argument, Operation, OperationSchema + + +class OperationSerializer(serializers.ModelSerializer): + ''' Serializer: Operation data. ''' + class Meta: + ''' serializer metadata. ''' + model = Operation + fields = '__all__' + read_only_fields = ('id', 'oss') + + +class ArgumentSerializer(serializers.ModelSerializer): + ''' Serializer: Operation data. ''' + class Meta: + ''' serializer metadata. ''' + model = Argument + fields = ('operation', 'argument') + + + +class OperationPositionSerializer(serializers.ModelSerializer): + ''' Serializer: Positions of a single operations in OSS. ''' + class Meta: + ''' serializer metadata. ''' + model = Operation + fields = 'id', 'position_x', 'position_y' + + def validate(self, attrs): + oss = cast(LibraryItem, self.context['oss']) + operation = cast(Operation, self.instance) + if operation.oss != oss: + raise serializers.ValidationError({ + 'id': msg.operationNotOwned(oss.title) + }) + return attrs + + +class OperationCreateSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta creation. ''' + positions = serializers.ListField( + child=OperationPositionSerializer(), + default=[] + ) + + class Meta: + ''' serializer metadata. ''' + model = Operation + fields = \ + 'alias', 'operation_type', 'title', \ + 'comment', 'position_x', 'position_y' + + +class OperationSchemaSerializer(serializers.ModelSerializer): + ''' Serializer: Detailed data for OSS. ''' + items = serializers.ListField( + child=OperationSerializer() + ) + graph = serializers.ListField( + child=ArgumentSerializer() + ) + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + + def to_representation(self, instance: LibraryItem): + result = LibraryItemDetailsSerializer(instance).data + oss = OperationSchema(instance) + result['items'] = [] + for operation in oss.operations(): + result['items'].append(OperationSerializer(operation).data) + result['graph'] = [] + for argument in oss.arguments(): + result['graph'].append(ArgumentSerializer(argument).data) + result['substitutions'] = [] + for substitution in oss.substitutions().values( + 'original', + 'original__alias', + 'original__term_resolved', + 'substitution', + 'substitution__alias', + 'substitution__term_resolved', + 'transfer_term' + ): + result['substitutions'].append(substitution) + return result diff --git a/rsconcept/backend/apps/oss/serializers/schema_typing.py b/rsconcept/backend/apps/oss/serializers/schema_typing.py new file mode 100644 index 00000000..c0cf5b31 --- /dev/null +++ b/rsconcept/backend/apps/oss/serializers/schema_typing.py @@ -0,0 +1,10 @@ +''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' +from rest_framework import serializers + +from .data_access import OperationSchemaSerializer, OperationSerializer + + +class NewOperationResponse(serializers.Serializer): + ''' Serializer: Create operation response. ''' + new_operation = OperationSerializer() + oss = OperationSchemaSerializer() diff --git a/rsconcept/backend/apps/oss/urls.py b/rsconcept/backend/apps/oss/urls.py new file mode 100644 index 00000000..5399e8d7 --- /dev/null +++ b/rsconcept/backend/apps/oss/urls.py @@ -0,0 +1,12 @@ +''' Routing: Operation Schema. ''' +from django.urls import include, path +from rest_framework import routers + +from . import views + +library_router = routers.SimpleRouter(trailing_slash=False) +library_router.register('rsforms', views.OssViewSet, 'RSForm') + +urlpatterns = [ + path('', include(library_router.urls)), +] diff --git a/rsconcept/backend/apps/oss/views/__init__.py b/rsconcept/backend/apps/oss/views/__init__.py new file mode 100644 index 00000000..d9319816 --- /dev/null +++ b/rsconcept/backend/apps/oss/views/__init__.py @@ -0,0 +1,2 @@ +''' REST API: Endpoint processors. ''' +from .oss import OssViewSet diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py new file mode 100644 index 00000000..7d036e1f --- /dev/null +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -0,0 +1,110 @@ +''' Endpoints for OSS. ''' +from typing import cast + +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import generics +from rest_framework import status as c +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response + +from shared import permissions + +from .. import models as m +from .. import serializers as s + + +@extend_schema(tags=['OSS']) +@extend_schema_view() +class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): + ''' Endpoint: OperationSchema. ''' + queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.OPERATION_SCHEMA) + serializer_class = s.LibraryItemSerializer + + def _get_schema(self) -> m.OperationSchema: + return m.OperationSchema(cast(m.LibraryItem, self.get_object())) + + def get_permissions(self): + ''' Determine permission class. ''' + if self.action in [ + 'operation_create', + 'operation_delete' + ]: + permission_list = [permissions.ItemEditor] + elif self.action in ['details']: + permission_list = [permissions.ItemAnyone] + else: + permission_list = [permissions.Anyone] + return [permission() for permission in permission_list] + + @extend_schema( + summary='get operations data', + tags=['OSS'], + request=None, + responses={ + c.HTTP_200_OK: s.OperationSchemaSerializer, + c.HTTP_404_NOT_FOUND: None + } + ) + @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())) + return Response( + status=c.HTTP_200_OK, + data=serializer.data + ) + + @extend_schema( + summary='create operation', + tags=['OSS'], + request=s.OperationCreateSerializer(), + responses={ + c.HTTP_201_CREATED: s.NewOperationResponse, + c.HTTP_403_FORBIDDEN: None + } + ) + @action(detail=True, methods=['post'], url_path='operation-create') + def operation_create(self, request: Request, pk): + ''' Create new operation. ''' + schema = self._get_schema() + serializer = s.OperationCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + new_operation = schema.create_operation(*data) + schema.item.refresh_from_db() + response = Response( + status=c.HTTP_201_CREATED, + data={ + 'new_operation': s.OperationSerializer(new_operation).data, + 'schema': s.OperationSchemaSerializer(schema.item).data + } + ) + return response + + # @extend_schema( + # summary='delete operation', + # tags=['RSForm'], + # request=s.CstListSerializer, + # responses={ + # c.HTTP_200_OK: s.RSFormParseSerializer, + # c.HTTP_403_FORBIDDEN: None, + # c.HTTP_404_NOT_FOUND: None + # } + # ) + # @action(detail=True, methods=['patch'], url_path='operation-delete') + # def operation_delete(self, request: Request, pk): + # ''' Endpoint: Delete operation. ''' + # schema = self._get_schema() + # serializer = s.CstListSerializer( + # data=request.data, + # context={'schema': schema.item} + # ) + # serializer.is_valid(raise_exception=True) + # schema.delete_cst(serializer.validated_data['items']) + # schema.item.refresh_from_db() + # return Response( + # status=c.HTTP_200_OK, + # data=s.RSFormParseSerializer(schema.item).data + # ) diff --git a/rsconcept/backend/apps/rsform/migrations/0008_alter_libraryitem_item_type.py b/rsconcept/backend/apps/rsform/migrations/0008_alter_libraryitem_item_type.py new file mode 100644 index 00000000..ef7f844d --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0008_alter_libraryitem_item_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-07-17 09:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0007_location_and_flags'), + ] + + operations = [ + migrations.AlterField( + model_name='libraryitem', + name='item_type', + field=models.CharField(choices=[('rsform', 'Rsform'), ('oss', 'Operation Schema')], max_length=50, verbose_name='Тип'), + ), + ] diff --git a/rsconcept/backend/apps/rsform/models/LibraryItem.py b/rsconcept/backend/apps/rsform/models/LibraryItem.py index 70489f9c..d4daf365 100644 --- a/rsconcept/backend/apps/rsform/models/LibraryItem.py +++ b/rsconcept/backend/apps/rsform/models/LibraryItem.py @@ -9,6 +9,7 @@ from django.db.models import ( DateTimeField, ForeignKey, Model, + QuerySet, TextChoices, TextField ) @@ -113,18 +114,18 @@ class LibraryItem(Model): def get_absolute_url(self): return f'/api/library/{self.pk}' - def subscribers(self) -> list[Subscription]: + def subscribers(self) -> list[User]: ''' Get all subscribers for this item. ''' return [subscription.user for subscription in Subscription.objects.filter(item=self.pk).only('user')] - def versions(self) -> list[Version]: - ''' Get all Versions of this item. ''' - return list(Version.objects.filter(item=self.pk).order_by('-time_create')) - - def editors(self) -> list[Editor]: + def editors(self) -> list[User]: ''' Get all Editors of this item. ''' return [item.editor for item in Editor.objects.filter(item=self.pk).only('editor')] + def versions(self) -> QuerySet[Version]: + ''' Get all Versions of this item. ''' + return Version.objects.filter(item=self.pk).order_by('-time_create') + @transaction.atomic def save(self, *args, **kwargs): subscribe = not self.pk and self.owner diff --git a/rsconcept/backend/apps/rsform/models/api_RSForm.py b/rsconcept/backend/apps/rsform/models/api_RSForm.py index dee2f7e6..9790f6b0 100644 --- a/rsconcept/backend/apps/rsform/models/api_RSForm.py +++ b/rsconcept/backend/apps/rsform/models/api_RSForm.py @@ -7,7 +7,8 @@ from django.core.exceptions import ValidationError from django.db import transaction from django.db.models import QuerySet -from .. import messages as msg +from shared import messages as msg + from ..graph import Graph from .api_RSLanguage import ( extract_globals, diff --git a/rsconcept/backend/apps/rsform/models/api_RSLanguage.py b/rsconcept/backend/apps/rsform/models/api_RSLanguage.py index d32a8c4e..26daa1bb 100644 --- a/rsconcept/backend/apps/rsform/models/api_RSLanguage.py +++ b/rsconcept/backend/apps/rsform/models/api_RSLanguage.py @@ -6,7 +6,8 @@ from typing import Set, Tuple, cast import pyconcept -from .. import messages as msg +from shared import messages as msg + from .Constituenta import CstType _RE_GLOBALS = r'[XCSADFPT]\d+' # cspell:disable-line diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py index 12c2a4f5..897a27c9 100644 --- a/rsconcept/backend/apps/rsform/serializers/__init__.py +++ b/rsconcept/backend/apps/rsform/serializers/__init__.py @@ -22,6 +22,7 @@ from .data_access import ( InlineSynthesisSerializer, LibraryItemBaseSerializer, LibraryItemCloneSerializer, + LibraryItemDetailsSerializer, LibraryItemSerializer, RSFormParseSerializer, RSFormSerializer, diff --git a/rsconcept/backend/apps/rsform/serializers/basics.py b/rsconcept/backend/apps/rsform/serializers/basics.py index cc21c83f..6a047d5b 100644 --- a/rsconcept/backend/apps/rsform/serializers/basics.py +++ b/rsconcept/backend/apps/rsform/serializers/basics.py @@ -4,7 +4,8 @@ from typing import cast from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference from rest_framework import serializers -from .. import messages as msg +from shared import messages as msg + from ..models import AccessPolicy, validate_location diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 2861eff7..8ad9065c 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -7,7 +7,8 @@ from django.db import transaction from rest_framework import serializers from rest_framework.serializers import PrimaryKeyRelatedField as PKField -from .. import messages as msg +from shared import messages as msg + from ..models import Constituenta, CstType, LibraryItem, RSForm, Version from .basics import CstParseSerializer from .io_pyconcept import PyConceptAdapter diff --git a/rsconcept/backend/apps/rsform/serializers/io_files.py b/rsconcept/backend/apps/rsform/serializers/io_files.py index 00065236..afc59108 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_files.py +++ b/rsconcept/backend/apps/rsform/serializers/io_files.py @@ -2,7 +2,8 @@ from django.db import transaction from rest_framework import serializers -from .. import messages as msg +from shared import messages as msg + from ..models import Constituenta, LibraryItem, RSForm from ..utils import fix_old_references diff --git a/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py b/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py index 1aa79820..0b767b62 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py +++ b/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py @@ -4,7 +4,8 @@ from typing import Optional, Union, cast import pyconcept -from .. import messages as msg +from shared import messages as msg + from ..models import RSForm diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py b/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py index 33c91811..a766beb3 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_cctext.py @@ -1,7 +1,7 @@ ''' Testing views ''' from cctext import split_grams -from ..EndpointTester import EndpointTester, decl_endpoint +from shared.EndpointTester import EndpointTester, decl_endpoint class TestNaturalLanguageViews(EndpointTester): 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 a19cfc49..c5dc9f7e 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py @@ -1,7 +1,6 @@ ''' Testing API: Constituents. ''' from apps.rsform.models import Constituenta, CstType, RSForm - -from ..EndpointTester import EndpointTester, decl_endpoint +from shared.EndpointTester import EndpointTester, decl_endpoint class TestConstituentaAPI(EndpointTester): 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 fcd92781..2be20e52 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py @@ -11,9 +11,8 @@ from apps.rsform.models import ( RSForm, Subscription ) - -from ..EndpointTester import EndpointTester, decl_endpoint -from ..testing_utils import response_contains +from shared.EndpointTester import EndpointTester, decl_endpoint +from shared.testing_utils import response_contains class TestLibraryViewset(EndpointTester): 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 0cd9867f..67d94ad0 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py @@ -1,7 +1,6 @@ ''' Testing API: Operations. ''' from apps.rsform.models import Constituenta, CstType, RSForm - -from ..EndpointTester import EndpointTester, decl_endpoint +from shared.EndpointTester import EndpointTester, decl_endpoint class TestInlineSynthesis(EndpointTester): 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 ac385761..19ea4153 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -15,9 +15,8 @@ from apps.rsform.models import ( LocationHead, RSForm ) - -from ..EndpointTester import EndpointTester, decl_endpoint -from ..testing_utils import response_contains +from shared.EndpointTester import EndpointTester, decl_endpoint +from shared.testing_utils import response_contains class TestRSFormViewset(EndpointTester): diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py index 910e852e..03143bf7 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rslang.py @@ -1,5 +1,5 @@ ''' Testing views ''' -from ..EndpointTester import EndpointTester, decl_endpoint +from shared.EndpointTester import EndpointTester, decl_endpoint class TestRSLanguageViews(EndpointTester): 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 d2b273a7..788b2aec 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_versions.py @@ -7,8 +7,7 @@ from zipfile import ZipFile from rest_framework import status from apps.rsform.models import Constituenta, RSForm - -from ..EndpointTester import EndpointTester, decl_endpoint +from shared.EndpointTester import EndpointTester, decl_endpoint class TestVersionViews(EndpointTester): diff --git a/rsconcept/backend/apps/rsform/views/constituents.py b/rsconcept/backend/apps/rsform/views/constituents.py index cd772f1c..195a099c 100644 --- a/rsconcept/backend/apps/rsform/views/constituents.py +++ b/rsconcept/backend/apps/rsform/views/constituents.py @@ -2,8 +2,9 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import generics +from shared import permissions + from .. import models as m -from .. import permissions from .. import serializers as s diff --git a/rsconcept/backend/apps/rsform/views/library.py b/rsconcept/backend/apps/rsform/views/library.py index 4ce592af..491a2db5 100644 --- a/rsconcept/backend/apps/rsform/views/library.py +++ b/rsconcept/backend/apps/rsform/views/library.py @@ -13,8 +13,9 @@ from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response +from shared import permissions + from .. import models as m -from .. import permissions from .. import serializers as s diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 65136f53..6d589075 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -13,9 +13,10 @@ from rest_framework.decorators import action, api_view from rest_framework.request import Request from rest_framework.response import Response -from .. import messages as msg +from shared import messages as msg +from shared import permissions + from .. import models as m -from .. import permissions from .. import serializers as s from .. import utils diff --git a/rsconcept/backend/apps/rsform/views/versions.py b/rsconcept/backend/apps/rsform/views/versions.py index a5e69c9a..4aedbbb8 100644 --- a/rsconcept/backend/apps/rsform/views/versions.py +++ b/rsconcept/backend/apps/rsform/views/versions.py @@ -10,8 +10,9 @@ from rest_framework.decorators import action, api_view, permission_classes from rest_framework.request import Request from rest_framework.response import Response +from shared import permissions + from .. import models as m -from .. import permissions from .. import serializers as s from .. import utils diff --git a/rsconcept/backend/apps/users/messages.py b/rsconcept/backend/apps/users/messages.py deleted file mode 100644 index f142f37a..00000000 --- a/rsconcept/backend/apps/users/messages.py +++ /dev/null @@ -1,14 +0,0 @@ -''' Utility: Text messages. ''' -# pylint: skip-file - - -def passwordAuthFailed(): - return 'Неизвестное сочетание имени пользователя (email) и пароля' - - -def passwordsNotMatch(): - return 'Введенные пароли не совпадают' - - -def emailAlreadyTaken(): - return 'Пользователь с данным email уже существует' diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index d7ed3dd6..31f9c2e0 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -4,8 +4,8 @@ from django.contrib.auth.password_validation import validate_password from rest_framework import serializers from apps.rsform.models import Editor, Subscription +from shared import messages as msg -from . import messages as msg from . import models diff --git a/rsconcept/backend/apps/users/tests/t_views.py b/rsconcept/backend/apps/users/tests/t_views.py index c003a385..0c7d594e 100644 --- a/rsconcept/backend/apps/users/tests/t_views.py +++ b/rsconcept/backend/apps/users/tests/t_views.py @@ -1,8 +1,8 @@ ''' Testing API: users. ''' from rest_framework.test import APIClient, APITestCase -from apps.rsform.tests.EndpointTester import EndpointTester, decl_endpoint from apps.users.models import User +from shared.EndpointTester import EndpointTester, decl_endpoint class TestUserAPIViews(EndpointTester): diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index 2d2f0de2..5357c542 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -74,6 +74,7 @@ INSTALLED_APPS = [ 'apps.users', 'apps.rsform', + 'apps.oss', 'drf_spectacular', 'drf_spectacular_sidecar', diff --git a/rsconcept/backend/apps/rsform/tests/EndpointTester.py b/rsconcept/backend/shared/EndpointTester.py similarity index 100% rename from rsconcept/backend/apps/rsform/tests/EndpointTester.py rename to rsconcept/backend/shared/EndpointTester.py diff --git a/rsconcept/backend/shared/__init__.py b/rsconcept/backend/shared/__init__.py new file mode 100644 index 00000000..6246926d --- /dev/null +++ b/rsconcept/backend/shared/__init__.py @@ -0,0 +1 @@ +''' Utilities shared between applications. ''' diff --git a/rsconcept/backend/apps/rsform/messages.py b/rsconcept/backend/shared/messages.py similarity index 79% rename from rsconcept/backend/apps/rsform/messages.py rename to rsconcept/backend/shared/messages.py index 9469bbdb..dae5328e 100644 --- a/rsconcept/backend/apps/rsform/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -6,6 +6,10 @@ def constituentaNotOwned(title: str): return f'Конституента не принадлежит схеме: {title}' +def operationNotOwned(title: str): + return f'Операция не принадлежит схеме: {title}' + + def substitutionNotInList(): return 'Отождествляемая конституента отсутствует в списке' @@ -64,3 +68,15 @@ def constituentaNoStructure(): def missingFile(): return 'Отсутствует прикрепленный файл' + + +def passwordAuthFailed(): + return 'Неизвестное сочетание имени пользователя (email) и пароля' + + +def passwordsNotMatch(): + return 'Введенные пароли не совпадают' + + +def emailAlreadyTaken(): + return 'Пользователь с данным email уже существует' diff --git a/rsconcept/backend/apps/rsform/permissions.py b/rsconcept/backend/shared/permissions.py similarity index 80% rename from rsconcept/backend/apps/rsform/permissions.py rename to rsconcept/backend/shared/permissions.py index 7a163dec..a614cf52 100644 --- a/rsconcept/backend/apps/rsform/permissions.py +++ b/rsconcept/backend/shared/permissions.py @@ -11,16 +11,24 @@ from rest_framework.permissions import \ from rest_framework.request import Request from rest_framework.views import APIView -from . import models as m +from apps.rsform.models import ( + AccessPolicy, + Constituenta, + Editor, + LibraryItem, + Subscription, + Version +) +from apps.users.models import User -def _extract_item(obj: Any) -> m.LibraryItem: - if isinstance(obj, m.LibraryItem): +def _extract_item(obj: Any) -> LibraryItem: + if isinstance(obj, LibraryItem): return obj - elif isinstance(obj, m.Constituenta): - return cast(m.LibraryItem, obj.schema) - elif isinstance(obj, (m.Version, m.Subscription, m.Editor)): - return cast(m.LibraryItem, obj.item) + elif isinstance(obj, Constituenta): + return cast(LibraryItem, obj.schema) + elif isinstance(obj, (Version, Subscription, Editor)): + return cast(LibraryItem, obj.item) raise PermissionDenied({ 'message': 'Invalid type error. Please contact developers', 'object_id': obj.id @@ -60,10 +68,10 @@ class ItemEditor(ItemOwner): if request.user.is_anonymous: return False item = _extract_item(obj) - if m.Editor.objects.filter( + if Editor.objects.filter( item=item, - editor=cast(m.User, request.user) - ).exists() and item.access_policy != m.AccessPolicy.PRIVATE: + editor=cast(User, request.user) + ).exists() and item.access_policy != AccessPolicy.PRIVATE: return True return super().has_object_permission(request, view, obj) @@ -76,7 +84,7 @@ class ItemAnyone(ItemEditor): def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: item = _extract_item(obj) - if item.access_policy == m.AccessPolicy.PUBLIC: + if item.access_policy == AccessPolicy.PUBLIC: return True return super().has_object_permission(request, view, obj) diff --git a/rsconcept/backend/apps/rsform/tests/testing_utils.py b/rsconcept/backend/shared/testing_utils.py similarity index 100% rename from rsconcept/backend/apps/rsform/tests/testing_utils.py rename to rsconcept/backend/shared/testing_utils.py