Refactor backend API generation and admin UI

This commit is contained in:
IRBorisov 2023-09-22 23:26:22 +03:00
parent f21a01bbc0
commit b6f14fdbe1
17 changed files with 506 additions and 285 deletions

View File

@ -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)

View File

@ -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 . '''

View File

@ -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()

View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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 {

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)

View File

@ -67,9 +67,8 @@ extends IConstituentaMeta {
}
}
export interface IConstituentaID extends Pick<IConstituentaMeta, 'id'>{}
export interface IConstituentaList {
items: IConstituentaID[]
items: number[]
}
export interface ICstCreateData

View File

@ -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, () => {

View File

@ -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, () => {

View File

@ -220,7 +220,7 @@ export function postNewConstituenta(schema: string, request: FrontExchange<ICstC
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
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
});

View File

@ -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 = {

View File

@ -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() {

View File

@ -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