diff --git a/rsconcept/backend/apps/rsform/admin.py b/rsconcept/backend/apps/rsform/admin.py index 2d95688e..c5dbafd1 100644 --- a/rsconcept/backend/apps/rsform/admin.py +++ b/rsconcept/backend/apps/rsform/admin.py @@ -6,14 +6,29 @@ from . import models class ConstituentaAdmin(admin.ModelAdmin): ''' Admin model: Constituenta. ''' - + ordering = ['schema', 'order'] + list_display = ['schema', 'alias', 'term_resolved', 'definition_resolved'] + search_fields = ['term_resolved', 'definition_resolved'] class LibraryAdmin(admin.ModelAdmin): ''' Admin model: LibraryItem. ''' + date_hierarchy = 'time_update' + list_display = [ + 'alias', 'title', 'owner', + 'is_common', 'is_canonical', + 'time_update' + ] + list_filter = ['is_common', 'is_canonical', 'time_update'] + search_fields = ['alias', 'title'] class SubscriptionAdmin(admin.ModelAdmin): ''' Admin model: Subscriptions. ''' + list_display = ['id', 'item', 'user'] + search_fields = [ + 'item__title', 'item__alias', + 'user__username', 'user__first_name', 'user__last_name' + ] admin.site.register(models.Constituenta, ConstituentaAdmin) diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index af1f4609..18bfcd0f 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -122,7 +122,7 @@ class LibraryItem(Model): return f'{self.title}' def get_absolute_url(self): - return f'/api/library/{self.pk}/' + return f'/api/library/{self.pk}' def subscribers(self) -> list[User]: ''' Get all subscribers for this item . ''' diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index 4107cd1b..1cf8573c 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -27,11 +27,6 @@ class ExpressionSerializer(serializers.Serializer): expression = serializers.CharField() -class ResultTextSerializer(serializers.Serializer): - ''' Serializer: Text result of a function call. ''' - result = serializers.CharField() - - class TextSerializer(serializers.Serializer): ''' Serializer: Text with references. ''' text = serializers.CharField() @@ -46,20 +41,233 @@ class LibraryItemSerializer(serializers.ModelSerializer): read_only_fields = ('owner', 'id', 'item_type') +class FunctionArgSerializer(serializers.Serializer): + ''' Serializer: RSLang function argument type. ''' + alias = serializers.CharField() + typification = serializers.CharField() + + +class CstParseSerializer(serializers.Serializer): + ''' Serializer: Constituenta parse result. ''' + status = serializers.CharField() + valueClass = serializers.CharField() + typification = serializers.CharField() + syntaxTree = serializers.CharField() + args = serializers.ListField( + child=FunctionArgSerializer() + ) + + +class ErrorDescriptionSerializer(serializers.Serializer): + ''' Serializer: RSError description. ''' + errorType = serializers.IntegerField() + position = serializers.IntegerField() + isCritical = serializers.BooleanField() + params = serializers.ListField( + child=serializers.CharField() + ) + +class NodeDataSerializer(serializers.Serializer): + ''' Serializer: Node data. ''' + dataType = serializers.CharField() + value = serializers.CharField() + + +class ASTNodeSerializer(serializers.Serializer): + ''' Serializer: Syntax tree node. ''' + uid = serializers.IntegerField() + parent = serializers.IntegerField() # type: ignore + typeID = serializers.IntegerField() + start = serializers.IntegerField() + finish = serializers.IntegerField() + data = NodeDataSerializer() # type: ignore + + +class ExpressionParseSerializer(serializers.Serializer): + ''' Serializer: RSlang expression parse result. ''' + parseResult = serializers.BooleanField() + syntax = serializers.CharField() + typification = serializers.CharField() + valueClass = serializers.CharField() + astText = serializers.CharField() + ast = serializers.ListField( + child=ASTNodeSerializer() + ) + errors = serializers.ListField( # type: ignore + child=ErrorDescriptionSerializer() + ) + args = serializers.ListField( + child=FunctionArgSerializer() + ) + + class LibraryItemDetailsSerializer(serializers.ModelSerializer): ''' Serializer: LibraryItem detailed data. ''' + subscribers = serializers.SerializerMethodField() + class Meta: ''' serializer metadata. ''' model = LibraryItem fields = '__all__' read_only_fields = ('owner', 'id', 'item_type') - def to_representation(self, instance: LibraryItem): - result = super().to_representation(instance) - result['subscribers'] = [item.pk for item in instance.subscribers()] + def get_subscribers(self, instance: LibraryItem) -> list[int]: + return [item.pk for item in instance.subscribers()] + + +class ConstituentaSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta data. ''' + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = '__all__' + read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') + + def update(self, instance: Constituenta, validated_data) -> Constituenta: + schema = RSForm(instance.schema) + definition: Optional[str] = validated_data['definition_raw'] if 'definition_raw' in validated_data else None + term: Optional[str] = validated_data['term_raw'] if 'term_raw' in validated_data else None + term_changed = False + if definition is not None and definition != instance.definition_raw : + validated_data['definition_resolved'] = schema.resolver().resolve(definition) + if term is not None and term != instance.term_raw: + validated_data['term_resolved'] = schema.resolver().resolve(term) + if validated_data['term_resolved'] != instance.term_resolved: + validated_data['term_forms'] = [] + term_changed = validated_data['term_resolved'] != instance.term_resolved + result: Constituenta = super().update(instance, validated_data) + if term_changed: + schema.on_term_change([result.alias]) + result.refresh_from_db() + schema.item.save() return result +class CstCreateSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta creation. ''' + insert_after = serializers.IntegerField(required=False, allow_null=True) + + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after' + + +class CstRenameSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta renaming. ''' + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = 'id', 'alias', 'cst_type' + + def validate(self, attrs): + schema = cast(RSForm, self.context['schema']) + old_cst = Constituenta.objects.get(pk=self.initial_data['id']) + if old_cst.schema != schema.item: + raise serializers.ValidationError({ + 'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}' + }) + if old_cst.alias == self.initial_data['alias']: + raise serializers.ValidationError({ + 'alias': f'Имя конституенты должно отличаться от текущего: {self.initial_data["alias"]}' + }) + self.instance = old_cst + attrs['schema'] = schema.item + attrs['id'] = self.initial_data['id'] + return attrs + + +class CstListSerializer(serializers.Serializer): + ''' Serializer: List of constituents from one origin. ''' + items = serializers.ListField( + child=serializers.IntegerField() + ) + + def validate(self, attrs): + schema = self.context['schema'] + cstList = [] + for item in attrs['items']: + try: + cst = Constituenta.objects.get(pk=item) + except Constituenta.DoesNotExist as exception: + raise serializers.ValidationError( + {f"{item}": 'Конституента не существует'} + ) from exception + if cst.schema != schema.item: + raise serializers.ValidationError( + {'items': f'Конституенты должны относиться к данной схеме: {item}'}) + cstList.append(cst) + attrs['constituents'] = cstList + return attrs + + +class CstMoveSerializer(CstListSerializer): + ''' Serializer: Change constituenta position. ''' + move_to = serializers.IntegerField() + + +class TextPositionSerializer(serializers.Serializer): + ''' Serializer: Text position. ''' + start = serializers.IntegerField() + finish = serializers.IntegerField() + + +class ReferenceDataSerializer(serializers.Serializer): + ''' Serializer: Reference data - Union of all references. ''' + offset = serializers.IntegerField() + nominal = serializers.CharField() + entity = serializers.CharField() + form = serializers.CharField() + + +class ReferenceSerializer(serializers.Serializer): + ''' Serializer: Language reference. ''' + type = serializers.CharField() + data = ReferenceDataSerializer() # type: ignore + pos_input = TextPositionSerializer() + pos_output = TextPositionSerializer() + + +class ResolverSerializer(serializers.Serializer): + ''' Serializer: Resolver results serializer. ''' + input = serializers.CharField() + output = serializers.CharField() + refs = serializers.ListField( + child=ReferenceSerializer() + ) + + def to_representation(self, instance: Resolver) -> dict: + return { + 'input': instance.input, + 'output': instance.output, + 'refs': [{ + 'type': ref.ref.get_type().value, + 'data': self._get_reference_data(ref.ref), + 'resolved': ref.resolved, + 'pos_input': { + 'start': ref.pos_input.start, + 'finish': ref.pos_input.finish + }, + 'pos_output': { + 'start': ref.pos_output.start, + 'finish': ref.pos_output.finish + } + } for ref in instance.refs] + } + + @staticmethod + def _get_reference_data(ref: Reference) -> dict: + if ref.get_type() == ReferenceType.entity: + return { + 'entity': cast(EntityReference, ref).entity, + 'form': cast(EntityReference, ref).form + } + else: + return { + 'offset': cast(SyntacticReference, ref).offset, + 'nominal': cast(SyntacticReference, ref).nominal + } + class PyConceptAdapter: ''' RSForm adapter for interacting with pyconcept module. ''' def __init__(self, instance: RSForm): @@ -113,27 +321,54 @@ class PyConceptAdapter: class RSFormSerializer(serializers.ModelSerializer): ''' Serializer: Detailed data for RSForm. ''' + subscribers = serializers.ListField( + child=serializers.IntegerField() + ) + items = serializers.ListField( + child=ConstituentaSerializer() + ) + class Meta: ''' serializer metadata. ''' - model = RSForm + model = LibraryItem + fields = '__all__' - def to_representation(self, instance: RSForm): - result = LibraryItemDetailsSerializer(instance.item).data + def to_representation(self, instance: LibraryItem): + result = LibraryItemDetailsSerializer(instance).data + schema = RSForm(instance) result['items'] = [] - for cst in instance.constituents().order_by('order'): + for cst in schema.constituents().order_by('order'): result['items'].append(ConstituentaSerializer(cst).data) return result -class RSFormParseSerializer(serializers.ModelSerializer): - ''' Serializer: Detailed data for RSForm including parse. ''' +class CstDetailsSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta data including parse. ''' + parse = CstParseSerializer() + class Meta: ''' serializer metadata. ''' - model = RSForm + model = Constituenta + fields = '__all__' - def to_representation(self, instance: RSForm): + +class RSFormParseSerializer(serializers.ModelSerializer): + ''' Serializer: Detailed data for RSForm including parse. ''' + subscribers = serializers.ListField( + child=serializers.IntegerField() + ) + items = serializers.ListField( + child=CstDetailsSerializer() + ) + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + fields = '__all__' + + def to_representation(self, instance: LibraryItem): result = RSFormSerializer(instance).data - parse = PyConceptAdapter(instance).parse() + parse = PyConceptAdapter(RSForm(instance)).parse() for cst_data in result['items']: cst_data['parse'] = next( cst['parse'] for cst in parse['items'] @@ -302,142 +537,12 @@ class RSFormTRSSerializer(serializers.Serializer): cst.term_raw = '' cst.term_forms = [] - -class ConstituentaSerializer(serializers.ModelSerializer): - ''' Serializer: Constituenta data. ''' - class Meta: - ''' serializer metadata. ''' - model = Constituenta - fields = '__all__' - read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') - - def update(self, instance: Constituenta, validated_data) -> Constituenta: - schema = RSForm(instance.schema) - definition: Optional[str] = validated_data['definition_raw'] if 'definition_raw' in validated_data else None - term: Optional[str] = validated_data['term_raw'] if 'term_raw' in validated_data else None - term_changed = False - if definition is not None and definition != instance.definition_raw : - validated_data['definition_resolved'] = schema.resolver().resolve(definition) - if term is not None and term != instance.term_raw: - validated_data['term_resolved'] = schema.resolver().resolve(term) - if validated_data['term_resolved'] != instance.term_resolved: - validated_data['term_forms'] = [] - term_changed = validated_data['term_resolved'] != instance.term_resolved - result: Constituenta = super().update(instance, validated_data) - if term_changed: - schema.on_term_change([result.alias]) - result.refresh_from_db() - schema.item.save() - return result +class ResultTextResponse(serializers.Serializer): + ''' Serializer: Text result of a function call. ''' + result = serializers.CharField() -class CstStandaloneSerializer(serializers.ModelSerializer): - ''' Serializer: Constituenta in current context. ''' - id = serializers.IntegerField() - - class Meta: - ''' serializer metadata. ''' - model = Constituenta - exclude = ('schema', ) - - def validate(self, attrs): - try: - attrs['object'] = Constituenta.objects.get(pk=attrs['id']) - except Constituenta.DoesNotExist as exception: - raise serializers.ValidationError({f"{attrs['id']}": 'Конституента не существует'}) from exception - return attrs - - -class CstCreateSerializer(serializers.ModelSerializer): - ''' Serializer: Constituenta creation. ''' - insert_after = serializers.IntegerField(required=False, allow_null=True) - - class Meta: - ''' serializer metadata. ''' - model = Constituenta - fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after' - - -class CstRenameSerializer(serializers.ModelSerializer): - ''' Serializer: Constituenta renaming. ''' - class Meta: - ''' serializer metadata. ''' - model = Constituenta - fields = 'id', 'alias', 'cst_type' - - def validate(self, attrs): - schema = cast(RSForm, self.context['schema']) - old_cst = Constituenta.objects.get(pk=self.initial_data['id']) - if old_cst.schema != schema.item: - raise serializers.ValidationError({ - 'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}' - }) - if old_cst.alias == self.initial_data['alias']: - raise serializers.ValidationError({ - 'alias': f'Имя конституенты должно отличаться от текущего: {self.initial_data["alias"]}' - }) - self.instance = old_cst - attrs['schema'] = schema.item - attrs['id'] = self.initial_data['id'] - return attrs - - -class CstListSerializer(serializers.Serializer): - ''' Serializer: List of constituents from one origin. ''' - # TODO: fix schema - items = serializers.ListField( - child=CstStandaloneSerializer() - ) - - def validate(self, attrs): - schema = self.context['schema'] - cstList = [] - for item in attrs['items']: - cst = item['object'] - if cst.schema != schema.item: - raise serializers.ValidationError( - {'items': f'Конституенты должны относиться к данной схеме: {item}'}) - cstList.append(cst) - attrs['constituents'] = cstList - return attrs - - -class CstMoveSerializer(CstListSerializer): - ''' Serializer: Change constituenta position. ''' - move_to = serializers.IntegerField() - - -class ResolverSerializer(serializers.Serializer): - ''' Serializer: Resolver results serializer. ''' - # TODO: add schema - def to_representation(self, instance: Resolver) -> dict: - return { - 'input': instance.input, - 'output': instance.output, - 'refs': [{ - 'type': ref.ref.get_type().value, - 'data': self._get_reference_data(ref.ref), - 'resolved': ref.resolved, - 'pos_input': { - 'start': ref.pos_input.start, - 'finish': ref.pos_input.finish - }, - 'pos_output': { - 'start': ref.pos_output.start, - 'finish': ref.pos_output.finish - } - } for ref in instance.refs] - } - - @staticmethod - def _get_reference_data(ref: Reference) -> dict: - if ref.get_type() == ReferenceType.entity: - return { - 'entity': cast(EntityReference, ref).entity, - 'form': cast(EntityReference, ref).form - } - else: - return { - 'offset': cast(SyntacticReference, ref).offset, - 'nominal': cast(SyntacticReference, ref).nominal - } +class NewCstResponse(serializers.Serializer): + ''' Serializer: Create cst response. ''' + new_cst = ConstituentaSerializer() + schema = RSFormParseSerializer() diff --git a/rsconcept/backend/apps/rsform/tests/t_models.py b/rsconcept/backend/apps/rsform/tests/t_models.py index 0f3575f1..43834ce1 100644 --- a/rsconcept/backend/apps/rsform/tests/t_models.py +++ b/rsconcept/backend/apps/rsform/tests/t_models.py @@ -80,7 +80,7 @@ class TestLibraryItem(TestCase): testStr = 'Test123' item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title=testStr, owner=self.user1, alias='КС1') - self.assertEqual(item.get_absolute_url(), f'/api/library/{item.id}/') + self.assertEqual(item.get_absolute_url(), f'/api/library/{item.id}') def test_create_default(self): item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test') diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index d15e8cb4..0cc89ff3 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -224,19 +224,19 @@ class TestLibraryViewset(APITestCase): def test_subscriptions(self): response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 204) self.assertFalse(self.user in self.unowned.subscribers()) response = self.client.post(f'/api/library/{self.unowned.id}/subscribe') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 204) self.assertTrue(self.user in self.unowned.subscribers()) response = self.client.post(f'/api/library/{self.unowned.id}/subscribe') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 204) self.assertTrue(self.user in self.unowned.subscribers()) response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 204) self.assertFalse(self.user in self.unowned.subscribers()) @@ -458,14 +458,14 @@ class TestRSFormViewset(APITestCase): def test_delete_constituenta(self): schema = self.owned - data = json.dumps({'items': [{'id': 1337}]}) + data = json.dumps({'items': [1337]}) response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', data=data, content_type='application/json') self.assertEqual(response.status_code, 400) x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1) x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2) - data = json.dumps({'items': [{'id': x1.id}]}) + data = json.dumps({'items': [x1.id]}) response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', data=data, content_type='application/json') x2.refresh_from_db() @@ -477,21 +477,21 @@ class TestRSFormViewset(APITestCase): self.assertEqual(x2.order, 1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1) - data = json.dumps({'items': [{'id': x3.id}]}) + data = json.dumps({'items': [x3.id]}) response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', data=data, content_type='application/json') self.assertEqual(response.status_code, 400) def test_move_constituenta(self): item = self.owned.item - data = json.dumps({'items': [{'id': 1337}], 'move_to': 1}) + data = json.dumps({'items': [1337], 'move_to': 1}) response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', data=data, content_type='application/json') self.assertEqual(response.status_code, 400) x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2) - data = json.dumps({'items': [{'id': x2.id}], 'move_to': 1}) + data = json.dumps({'items': [x2.id], 'move_to': 1}) response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', data=data, content_type='application/json') x1.refresh_from_db() @@ -502,7 +502,7 @@ class TestRSFormViewset(APITestCase): self.assertEqual(x2.order, 1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1) - data = json.dumps({'items': [{'id': x3.id}], 'move_to': 1}) + data = json.dumps({'items': [x3.id], 'move_to': 1}) response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', data=data, content_type='application/json') self.assertEqual(response.status_code, 400) diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 5dac29b2..91ed6036 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -10,6 +10,7 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.decorators import api_view from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import status as c import pyconcept from . import models as m @@ -84,11 +85,14 @@ class LibraryViewSet(viewsets.ModelViewSet): def _get_item(self) -> m.LibraryItem: return cast(m.LibraryItem, self.get_object()) - # TODO: response schema @extend_schema( - request=s.LibraryItemSerializer, summary='clone item including contents', - tags=['Library'] + tags=['Library'], + request=s.LibraryItemSerializer, + responses={ + c.HTTP_201_CREATED: s.RSFormParseSerializer, + c.HTTP_404_NOT_FOUND: None + } ) @transaction.atomic @action(detail=True, methods=['post'], url_path='clone') @@ -110,14 +114,17 @@ class LibraryViewSet(viewsets.ModelViewSet): clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True}) clone.is_valid(raise_exception=True) new_schema = clone.save() - return Response(status=201, data=s.RSFormParseSerializer(new_schema).data) - return Response(status=404) + return Response( + status=c.HTTP_201_CREATED, + data=s.RSFormParseSerializer(new_schema.item).data + ) + return Response(status=c.HTTP_404_NOT_FOUND) @extend_schema( - request=None, - responses={200: s.LibraryItemSerializer}, summary='claim item', - tags=['Library'] + tags=['Library'], + request=None, + responses={c.HTTP_200_OK: s.LibraryItemSerializer} ) @transaction.atomic @action(detail=True, methods=['post']) @@ -130,33 +137,36 @@ class LibraryViewSet(viewsets.ModelViewSet): item.owner = self.request.user item.save() m.Subscription.subscribe(user=item.owner, item=item) - return Response(status=200, data=s.LibraryItemSerializer(item).data) + return Response( + status=c.HTTP_200_OK, + data=s.LibraryItemSerializer(item).data + ) @extend_schema( - request=None, - responses={200: None}, summary='subscribe to item', - tags=['Library'] + tags=['Library'], + request=None, + responses={c.HTTP_204_NO_CONTENT: None} ) @action(detail=True, methods=['post']) def subscribe(self, request, pk): ''' Endpoint: Subscribe current user to item. ''' item = self._get_item() m.Subscription.subscribe(user=self.request.user, item=item) - return Response(status=200) + return Response(status=c.HTTP_204_NO_CONTENT) @extend_schema( - request=None, - responses={200: None}, summary='unsubscribe from item', - tags=['Library'] + tags=['Library'], + request=None, + responses={c.HTTP_204_NO_CONTENT: None}, ) @action(detail=True, methods=['delete']) def unsubscribe(self, request, pk): ''' Endpoint: Unsubscribe current user from item. ''' item = self._get_item() m.Subscription.unsubscribe(user=self.request.user, item=item) - return Response(status=200) + return Response(status=c.HTTP_204_NO_CONTENT) @extend_schema(tags=['RSForm']) @@ -178,11 +188,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr permission_classes = [permissions.AllowAny] return [permission() for permission in permission_classes] - # TODO: response schema @extend_schema( - request=s.CstCreateSerializer, summary='create constituenta', - tags=['Constituenta'] + tags=['Constituenta'], + request=s.CstCreateSerializer, + responses={c.HTTP_201_CREATED: s.NewCstResponse} ) @action(detail=True, methods=['post'], url_path='cst-create') def cst_create(self, request, pk): @@ -193,18 +203,21 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr data = serializer.validated_data new_cst = schema.create_cst(data, data['insert_after'] if 'insert_after' in data else None) schema.item.refresh_from_db() - response = Response(status=201, data={ - 'new_cst': s.ConstituentaSerializer(new_cst).data, - 'schema': s.RSFormParseSerializer(schema).data - }) + response = Response( + status=c.HTTP_201_CREATED, + data={ + 'new_cst': s.ConstituentaSerializer(new_cst).data, + 'schema': s.RSFormParseSerializer(schema.item).data + } + ) response['Location'] = new_cst.get_absolute_url() return response - # TODO: response schema @extend_schema( - request=s.CstRenameSerializer, summary='rename constituenta', - tags=['Constituenta'] + tags=['Constituenta'], + request=s.CstRenameSerializer, + responses={c.HTTP_200_OK: s.NewCstResponse} ) @transaction.atomic @action(detail=True, methods=['patch'], url_path='cst-rename') @@ -219,16 +232,19 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema.apply_mapping(mapping, change_aliases=False) schema.item.refresh_from_db() cst = m.Constituenta.objects.get(pk=serializer.validated_data['id']) - return Response(status=200, data={ - 'new_cst': s.ConstituentaSerializer(cst).data, - 'schema': s.RSFormParseSerializer(schema).data - }) + return Response( + status=c.HTTP_200_OK, + data={ + 'new_cst': s.ConstituentaSerializer(cst).data, + 'schema': s.RSFormParseSerializer(schema.item).data + } + ) - # TODO: response schema @extend_schema( - request=s.CstListSerializer, summary='delete constituents', - tags=['Constituenta'] + tags=['Constituenta'], + request=s.CstListSerializer, + responses={c.HTTP_202_ACCEPTED: s.RSFormParseSerializer} ) @action(detail=True, methods=['patch'], url_path='cst-multidelete') def cst_multidelete(self, request, pk): @@ -238,13 +254,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr serializer.is_valid(raise_exception=True) schema.delete_cst(serializer.validated_data['constituents']) schema.item.refresh_from_db() - return Response(status=202, data=s.RSFormParseSerializer(schema).data) + return Response( + status=c.HTTP_202_ACCEPTED, + data=s.RSFormParseSerializer(schema.item).data + ) - # TODO: response schema @extend_schema( - request=s.CstMoveSerializer, summary='move constituenta', - tags=['Constituenta'] + tags=['Constituenta'], + request=s.CstMoveSerializer, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} ) @action(detail=True, methods=['patch'], url_path='cst-moveto') def cst_moveto(self, request, pk): @@ -254,26 +273,32 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr serializer.is_valid(raise_exception=True) schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to']) schema.item.refresh_from_db() - return Response(status=200, data=s.RSFormParseSerializer(schema).data) + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) - # TODO: response schema @extend_schema( - request=None, summary='reset aliases, update expressions and references', - tags=['RSForm'] + tags=['RSForm'], + request=None, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} ) @action(detail=True, methods=['patch'], url_path='reset-aliases') def reset_aliases(self, request, pk): ''' Endpoint: Recreate all aliases based on order. ''' schema = self._get_schema() schema.reset_aliases() - return Response(status=200, data=s.RSFormParseSerializer(schema).data) + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) - # TODO: response schema @extend_schema( - request=s.RSFormUploadSerializer, summary='load data from TRS file', - tags=['RSForm'] + tags=['RSForm'], + request=s.RSFormUploadSerializer, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} ) @action(detail=True, methods=['patch'], url_path='load-trs') def load_trs(self, request, pk): @@ -288,38 +313,47 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata}) serializer.is_valid(raise_exception=True) schema = serializer.save() - return Response(status=200, data=s.RSFormParseSerializer(schema).data) + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) - # TODO: response schema @extend_schema( - request=None, summary='get all constituents data from DB', - tags=['RSForm'] + tags=['RSForm'], + request=None, + responses={c.HTTP_200_OK: s.RSFormSerializer} ) @action(detail=True, methods=['get']) def contents(self, request, pk): ''' Endpoint: View schema db contents (including constituents). ''' - schema = s.RSFormSerializer(self._get_schema()).data - return Response(schema) + schema = s.RSFormSerializer(self.get_object()) + return Response( + status=c.HTTP_200_OK, + data=schema.data + ) - # TODO: response schema @extend_schema( - request=None, summary='get all constituents data and parses', - tags=['RSForm'] + tags=['RSForm'], + request=None, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} ) @action(detail=True, methods=['get']) def details(self, request, pk): ''' Endpoint: Detailed schema view including statuses and parse. ''' schema = self._get_schema() - serializer = s.RSFormParseSerializer(schema) - return Response(serializer.data) + serializer = s.RSFormParseSerializer(schema.item) + return Response( + status=c.HTTP_200_OK, + data=serializer.data + ) - # TODO: response schema @extend_schema( - request=s.ExpressionSerializer, summary='check RSLang expression', - tags=['RSForm', 'Functions'] + tags=['RSForm', 'FormalLanguage'], + request=s.ExpressionSerializer, + responses={c.HTTP_200_OK: s.ExpressionParseSerializer}, ) @action(detail=True, methods=['post']) def check(self, request, pk): @@ -329,13 +363,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr expression = serializer.validated_data['expression'] schema = s.PyConceptAdapter(self._get_schema()) result = pyconcept.check_expression(json.dumps(schema.data), expression) - return Response(json.loads(result)) + return Response( + status=c.HTTP_200_OK, + data=json.loads(result) + ) @extend_schema( - request=s.TextSerializer, - responses={200: s.ResolverSerializer}, summary='resolve text with references', - tags=['RSForm', 'Functions'] + tags=['RSForm', 'NaturalLanguage'], + request=s.TextSerializer, + responses={c.HTTP_200_OK: s.ResolverSerializer} ) @action(detail=True, methods=['post']) def resolve(self, request, pk): @@ -345,13 +382,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr text = serializer.validated_data['text'] resolver = self._get_schema().resolver() resolver.resolve(text) - return Response(status=200, data=s.ResolverSerializer(resolver).data) + return Response( + status=c.HTTP_200_OK, + data=s.ResolverSerializer(resolver).data + ) - # TODO: create a proper file response schema @extend_schema( - responses={(200, 'application/zip'): bytes}, summary='export as TRS file', - tags=['RSForm'] + tags=['RSForm'], + request=None, + responses={(c.HTTP_200_OK, 'application/zip'): bytes} ) @action(detail=True, methods=['get'], url_path='export-trs') def export_trs(self, request, pk): @@ -376,7 +416,9 @@ class TrsImportView(views.APIView): @extend_schema( summary='import TRS file into RSForm', - tags=['RSForm'] + tags=['RSForm'], + request=s.FileSerializer, + responses={c.HTTP_201_CREATED: s.LibraryItemSerializer} ) def post(self, request): data = utils.read_trs(request.FILES['file'].file) @@ -388,12 +430,17 @@ class TrsImportView(views.APIView): serializer.is_valid(raise_exception=True) schema = serializer.save() result = s.LibraryItemSerializer(schema.item) - return Response(status=201, data=result.data) + return Response( + status=c.HTTP_201_CREATED, + data=result.data + ) @extend_schema( summary='create new RSForm empty or from file', - tags=['RSForm'] + tags=['RSForm'], + request=s.LibraryItemSerializer, + responses={c.HTTP_201_CREATED: s.LibraryItemSerializer} ) @api_view(['POST']) def create_rsform(request): @@ -419,7 +466,10 @@ def create_rsform(request): serializer.is_valid(raise_exception=True) schema = serializer.save() result = s.LibraryItemSerializer(schema.item) - return Response(status=201, data=result.data) + return Response( + status=c.HTTP_201_CREATED, + data=result.data + ) def _prepare_rsform_data(data: dict, request, owner: m.User): data['owner'] = owner @@ -443,11 +493,12 @@ def _prepare_rsform_data(data: dict, request, owner: m.User): data['is_canonical'] = is_canonical -# TODO: define schema for response @extend_schema( - request=s.ExpressionSerializer, summary='RS expression into Syntax Tree', - tags=['Functions'] + tags=['FormalLanguage'], + request=s.ExpressionSerializer, + responses={c.HTTP_200_OK: s.ExpressionParseSerializer}, + auth=None ) @api_view(['POST']) def parse_expression(request): @@ -456,14 +507,18 @@ def parse_expression(request): serializer.is_valid(raise_exception=True) expression = serializer.validated_data['expression'] result = pyconcept.parse_expression(expression) - return Response(json.loads(result)) + return Response( + status=c.HTTP_200_OK, + data=json.loads(result) + ) @extend_schema( - request=s.ExpressionSerializer, - responses={200: s.ResultTextSerializer}, summary='Unicode syntax to ASCII TeX', - tags=['Functions'] + tags=['FormalLanguage'], + request=s.ExpressionSerializer, + responses={c.HTTP_200_OK: s.ResultTextResponse}, + auth=None ) @api_view(['POST']) def convert_to_ascii(request): @@ -476,10 +531,11 @@ def convert_to_ascii(request): @extend_schema( - request=s.ExpressionSerializer, - responses={200: s.ResultTextSerializer}, summary='ASCII TeX syntax to Unicode symbols', - tags=['Functions'] + tags=['FormalLanguage'], + request=s.ExpressionSerializer, + responses={200: s.ResultTextResponse}, + auth=None ) @api_view(['POST']) def convert_to_math(request): diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index e1b6607f..d2206788 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -7,9 +7,15 @@ from apps.rsform.models import Subscription from . import models +class NonFieldErrorSerializer(serializers.Serializer): + ''' Serializer: list of non-field errors. ''' + non_field_errors = serializers.ListField( + child=serializers.CharField() + ) + + class LoginSerializer(serializers.Serializer): ''' Serializer: User authentification by login/password. ''' - # TODO: declare schema username = serializers.CharField( label='Имя пользователя', write_only=True @@ -44,7 +50,13 @@ class LoginSerializer(serializers.Serializer): class AuthSerializer(serializers.Serializer): ''' Serializer: Authentication data. ''' - # TODO: declare schema + id = serializers.IntegerField() + username = serializers.CharField() + is_staff = serializers.BooleanField() + subscriptions = serializers.ListField( + child=serializers.IntegerField() + ) + def to_representation(self, instance: models.User) -> dict: if instance.is_anonymous: return { diff --git a/rsconcept/backend/apps/users/views.py b/rsconcept/backend/apps/users/views.py index 04d5a47e..6597d4a9 100644 --- a/rsconcept/backend/apps/users/views.py +++ b/rsconcept/backend/apps/users/views.py @@ -1,40 +1,52 @@ ''' REST API: User profile and Authentification. ''' from django.contrib.auth import login, logout -from rest_framework import status, permissions, views, generics +from rest_framework import status as c +from rest_framework import permissions, views, generics from rest_framework.response import Response from drf_spectacular.utils import extend_schema, extend_schema_view -from . import serializers -from . import models +from . import serializers as s +from . import models as m -@extend_schema(tags=['Auth']) -@extend_schema_view() class LoginAPIView(views.APIView): ''' Endpoint: Login via username + password. ''' permission_classes = (permissions.AllowAny,) + @extend_schema( + summary='login user', + tags=['Auth'], + request=s.LoginSerializer, + responses={ + c.HTTP_202_ACCEPTED: None, + c.HTTP_400_BAD_REQUEST: s.NonFieldErrorSerializer + } + ) def post(self, request): - serializer = serializers.LoginSerializer( + serializer = s.LoginSerializer( data=self.request.data, context={'request': self.request} ) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] login(request, user) - return Response(None, status=status.HTTP_202_ACCEPTED) + return Response(None, status=c.HTTP_202_ACCEPTED) -@extend_schema(tags=['Auth']) -@extend_schema_view() class LogoutAPIView(views.APIView): ''' Endpoint: Logout. ''' permission_classes = (permissions.IsAuthenticated,) + @extend_schema( + summary='logout current user', + tags=['Auth'], + request=None, + responses={c.HTTP_204_NO_CONTENT: None} + ) def post(self, request): logout(request) - return Response(None, status=status.HTTP_204_NO_CONTENT) + return Response(None, status=c.HTTP_204_NO_CONTENT) @extend_schema(tags=['User']) @@ -42,7 +54,7 @@ class LogoutAPIView(views.APIView): class SignupAPIView(generics.CreateAPIView): ''' Endpoint: Register user. ''' permission_classes = (permissions.AllowAny, ) - serializer_class = serializers.SignupSerializer + serializer_class = s.SignupSerializer @extend_schema(tags=['Auth']) @@ -50,7 +62,7 @@ class SignupAPIView(generics.CreateAPIView): class AuthAPIView(generics.RetrieveAPIView): ''' Endpoint: Current user info. ''' permission_classes = (permissions.AllowAny,) - serializer_class = serializers.AuthSerializer + serializer_class = s.AuthSerializer def get_object(self): return self.request.user @@ -61,10 +73,10 @@ class AuthAPIView(generics.RetrieveAPIView): class ActiveUsersView(generics.ListAPIView): ''' Endpoint: Get list of active users. ''' permission_classes = (permissions.AllowAny,) - serializer_class = serializers.UserSerializer + serializer_class = s.UserSerializer def get_queryset(self): - return models.User.objects.filter(is_active=True) + return m.User.objects.filter(is_active=True) @extend_schema(tags=['User']) @@ -72,14 +84,12 @@ class ActiveUsersView(generics.ListAPIView): class UserProfileAPIView(generics.RetrieveUpdateAPIView): ''' Endpoint: User profile. ''' permission_classes = (permissions.IsAuthenticated,) - serializer_class = serializers.UserSerializer + serializer_class = s.UserSerializer def get_object(self): return self.request.user -@extend_schema(tags=['Auth']) -@extend_schema_view() class UpdatePassword(views.APIView): ''' Endpoint: Change password for current user. ''' permission_classes = (permissions.IsAuthenticated, ) @@ -87,17 +97,25 @@ class UpdatePassword(views.APIView): def get_object(self, queryset=None): return self.request.user + @extend_schema( + description='change current user password', + tags=['Auth'], + request=s.ChangePasswordSerializer, + responses={ + c.HTTP_204_NO_CONTENT: None, + c.HTTP_400_BAD_REQUEST: None + } + ) def patch(self, request, *args, **kwargs): self.object = self.get_object() - serializer = serializers.ChangePasswordSerializer(data=request.data) + serializer = s.ChangePasswordSerializer(data=request.data) if serializer.is_valid(): old_password = serializer.data.get("old_password") if not self.object.check_password(old_password): return Response({"old_password": ["Wrong password."]}, - status=status.HTTP_400_BAD_REQUEST) + status=c.HTTP_400_BAD_REQUEST) # Note: set_password also hashes the password that the user will get self.object.set_password(serializer.data.get("new_password")) self.object.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(status=c.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=c.HTTP_400_BAD_REQUEST) diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index c38a3fb6..bb3ab66b 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -86,9 +86,10 @@ MIDDLEWARE = [ ] ROOT_URLCONF = 'project.urls' -LOGIN_URL = '/admin/login/' +LOGIN_URL = '/admin/login' LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' +APPEND_SLASH = False # Static files (CSS, JavaScript, Images) @@ -140,6 +141,20 @@ SPECTACULAR_SETTINGS = { 'DESCRIPTION': 'Портал для работы с экспликациями концептуальных схем', 'VERSION': '0.1.0', 'SERVE_INCLUDE_SCHEMA': False, + + 'COMPONENT_SPLIT_PATCH': True, + 'COMPONENT_SPLIT_REQUEST': True, + + 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], + 'SERVE_AUTHENTICATION': None, + + 'DISABLE_ERRORS_AND_WARNINGS': False, + + "SWAGGER_UI_SETTINGS": { + "deepLinking": True, + "persistAuthorization": True, + "withCredentials": True + } } diff --git a/rsconcept/backend/project/urls.py b/rsconcept/backend/project/urls.py index f83ac539..da4fd21c 100644 --- a/rsconcept/backend/project/urls.py +++ b/rsconcept/backend/project/urls.py @@ -11,8 +11,8 @@ urlpatterns = [ path('admin', admin.site.urls), path('api/', include('apps.rsform.urls')), path('users/', include('apps.users.urls')), - path('docs', SpectacularSwaggerView.as_view(), name='docs'), + path('docs/', SpectacularSwaggerView.as_view(), name='docs'), path('schema', SpectacularAPIView.as_view(), name='schema'), path('redoc', SpectacularRedocView.as_view()), - path('', lambda: redirect('docs', permanent=True)), + path('', lambda: redirect('docs/', permanent=True)), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/rsconcept/frontend/src/models/rsform.ts b/rsconcept/frontend/src/models/rsform.ts index b34548cd..f5f3a4ea 100644 --- a/rsconcept/frontend/src/models/rsform.ts +++ b/rsconcept/frontend/src/models/rsform.ts @@ -67,9 +67,8 @@ extends IConstituentaMeta { } } -export interface IConstituentaID extends Pick{} export interface IConstituentaList { - items: IConstituentaID[] + items: number[] } export interface ICstCreateData diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx index 04c9fcbb..e680ce66 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx @@ -64,7 +64,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) }, -1); const target = Math.max(0, currentIndex - 1) + 1 const data = { - items: selected.map(id => ({ id: id })), + items: selected, move_to: target } cstMoveTo(data, () => { @@ -95,7 +95,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) }, -1); const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1 const data: ICstMovetoData = { - items: selected.map(id => ({ id: id })), + items: selected, move_to: target } cstMoveTo(data, () => { diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index 0e7bffd3..4c22c977 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -180,7 +180,7 @@ function RSTabs() { return; } const data = { - items: deleted.map(id => ({ id: id })) + items: deleted }; let activeIndex = schema.items.findIndex(cst => cst.id === activeID); cstDelete(data, () => { diff --git a/rsconcept/frontend/src/utils/backendAPI.ts b/rsconcept/frontend/src/utils/backendAPI.ts index 71cfae9d..e81e832d 100644 --- a/rsconcept/frontend/src/utils/backendAPI.ts +++ b/rsconcept/frontend/src/utils/backendAPI.ts @@ -220,7 +220,7 @@ export function postNewConstituenta(schema: string, request: FrontExchange) { AxiosPatch({ - title: `Delete Constituents for RSForm id=${schema}: ${request.data.items.map(item => String(item.id)).join(' ')}`, + title: `Delete Constituents for RSForm id=${schema}: ${request.data.items.map(item => String(item)).join(' ')}`, endpoint: `/api/rsforms/${schema}/cst-multidelete`, request: request }); diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 9bde6954..9cec7c85 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -21,7 +21,7 @@ export const urls = { gitrepo: 'https://github.com/IRBorisov/ConceptPortal', mailportal: 'mailto:portal@acconcept.ru', - restapi: 'https://api.portal.acconcept.ru/docs' + restapi: 'https://api.portal.acconcept.ru/docs/' }; export const resources = { diff --git a/scripts/dev/RunServer.ps1 b/scripts/dev/RunServer.ps1 index d98b949a..0484bb43 100644 --- a/scripts/dev/RunServer.ps1 +++ b/scripts/dev/RunServer.ps1 @@ -14,8 +14,8 @@ function RunServer() { BackendRun FrontendRun Start-Sleep -Seconds 1 - Start-Process "http://localhost:8000/" - Start-Process "http://localhost:3000/" + Start-Process "http://localhost:8000" + Start-Process "http://localhost:3000" } function BackendRun() { diff --git a/scripts/prod/UpdateProd.sh b/scripts/prod/UpdateProd.sh index 6035c537..d36440b2 100644 --- a/scripts/prod/UpdateProd.sh +++ b/scripts/prod/UpdateProd.sh @@ -8,4 +8,5 @@ git pull docker compose --file "${COMPOSE_FILE}" up --build --detach docker image prune --all --force +sleep 5 docker compose --file "${COMPOSE_FILE}" restart \ No newline at end of file