ConceptPortal-public/rsconcept/backend/apps/rsform/views.py

365 lines
16 KiB
Python
Raw Normal View History

''' REST API: RSForms for conceptual schemas. '''
2023-07-15 17:46:19 +03:00
import json
2023-08-25 22:51:20 +03:00
from typing import cast
2023-08-23 12:15:16 +03:00
from django.db import transaction
2023-07-15 17:46:19 +03:00
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
2023-08-01 20:14:03 +03:00
from django.db.models import Q
2023-07-18 14:55:40 +03:00
from rest_framework import views, viewsets, filters, generics, permissions
2023-07-15 17:46:19 +03:00
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.decorators import api_view
import pyconcept
2023-08-25 22:51:20 +03:00
from . import models as m
from . import serializers as s
2023-07-15 17:46:19 +03:00
from . import utils
2023-08-25 22:51:20 +03:00
class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of rsforms available for active user. '''
2023-08-01 20:14:03 +03:00
permission_classes = (permissions.AllowAny,)
2023-08-25 22:51:20 +03:00
serializer_class = s.LibraryItemSerializer
2023-08-01 20:14:03 +03:00
def get_queryset(self):
user = self.request.user
if not user.is_anonymous:
2023-08-27 15:39:49 +03:00
# pylint: disable=unsupported-binary-operation
2023-09-01 23:44:02 +03:00
return m.LibraryItem.objects.filter(
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
2023-09-10 20:17:18 +03:00
).distinct().order_by('-time_update')
2023-08-01 20:14:03 +03:00
else:
2023-09-10 20:17:18 +03:00
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
2023-08-01 20:14:03 +03:00
2023-07-18 14:55:40 +03:00
class ConstituentAPIView(generics.RetrieveUpdateAPIView):
''' Endpoint: Get / Update Constituenta. '''
2023-08-25 22:51:20 +03:00
queryset = m.Constituenta.objects.all()
serializer_class = s.ConstituentaSerializer
2023-07-18 14:55:40 +03:00
def get_permissions(self):
result = super().get_permissions()
if self.request.method.lower() == 'get':
result.append(permissions.AllowAny())
else:
result.append(utils.SchemaOwnerOrAdmin())
return result
# pylint: disable=too-many-ancestors
2023-08-25 22:51:20 +03:00
class LibraryViewSet(viewsets.ModelViewSet):
''' Endpoint: Library operations. '''
queryset = m.LibraryItem.objects.all()
serializer_class = s.LibraryItemSerializer
2023-07-15 17:46:19 +03:00
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
2023-08-25 22:51:20 +03:00
filterset_fields = ['item_type', 'owner', 'is_common', 'is_canonical']
ordering_fields = ('item_type', 'owner', 'title', 'time_update')
ordering = '-time_update'
2023-07-15 17:46:19 +03:00
def perform_create(self, serializer):
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
return serializer.save(owner=self.request.user)
else:
return serializer.save()
def get_permissions(self):
2023-08-25 22:51:20 +03:00
if self.action in ['update', 'destroy', 'partial_update']:
2023-07-15 17:46:19 +03:00
permission_classes = [utils.ObjectOwnerOrAdmin]
2023-08-26 17:26:49 +03:00
elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']:
2023-07-15 17:46:19 +03:00
permission_classes = [permissions.IsAuthenticated]
2023-08-25 22:51:20 +03:00
elif self.action in ['claim']:
permission_classes = [utils.IsClaimable]
else:
permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes]
2023-08-26 17:26:49 +03:00
def _get_item(self) -> m.LibraryItem:
return cast(m.LibraryItem, self.get_object())
2023-08-25 22:51:20 +03:00
@transaction.atomic
@action(detail=True, methods=['post'], url_path='clone')
def clone(self, request, pk):
''' Endpoint: Create deep copy of library item. '''
serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
2023-08-26 17:26:49 +03:00
item = self._get_item()
2023-08-25 22:51:20 +03:00
if item.item_type == m.LibraryItemType.RSFORM:
schema = m.RSForm(item)
clone_data = s.RSFormTRSSerializer(schema).data
clone_data['item_type'] = item.item_type
clone_data['owner'] = self.request.user
clone_data['title'] = serializer.validated_data['title']
clone_data['alias'] = serializer.validated_data.get('alias', '')
clone_data['comment'] = serializer.validated_data.get('comment', '')
clone_data['is_common'] = serializer.validated_data.get('is_common', False)
clone_data['is_canonical'] = serializer.validated_data.get('is_canonical', False)
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)
2023-08-25 22:51:20 +03:00
return Response(status=404)
2023-08-26 17:26:49 +03:00
@transaction.atomic
2023-08-25 22:51:20 +03:00
@action(detail=True, methods=['post'])
def claim(self, request, pk=None):
''' Endpoint: Claim ownership of LibraryItem. '''
2023-08-26 17:26:49 +03:00
item = self._get_item()
2023-08-25 22:51:20 +03:00
if item.owner == self.request.user:
return Response(status=304)
else:
item.owner = self.request.user
item.save()
2023-08-26 17:26:49 +03:00
m.Subscription.subscribe(user=item.owner, item=item)
2023-08-25 22:51:20 +03:00
return Response(status=200, data=s.LibraryItemSerializer(item).data)
2023-08-26 17:26:49 +03:00
@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)
@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)
2023-08-25 22:51:20 +03:00
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. '''
2023-08-26 17:26:49 +03:00
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM)
2023-08-25 22:51:20 +03:00
serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.RSForm:
return m.RSForm(self.get_object()) # type: ignore
def get_permissions(self):
''' Determine permission class. '''
if self.action in ['load_trs', 'cst_create', 'cst_multidelete',
'reset_aliases', 'cst_rename']:
permission_classes = [utils.ObjectOwnerOrAdmin]
2023-07-15 17:46:19 +03:00
else:
permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes]
@action(detail=True, methods=['post'], url_path='cst-create')
def cst_create(self, request, pk):
''' Create new constituenta. '''
2023-07-27 22:04:25 +03:00
schema = self._get_schema()
2023-08-25 22:51:20 +03:00
serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
new_cst = schema.create_cst(data, data['insert_after'] if 'insert_after' in data else None)
2023-08-25 22:51:20 +03:00
schema.item.refresh_from_db()
response = Response(status=201, data={
2023-08-25 22:51:20 +03:00
'new_cst': s.ConstituentaSerializer(new_cst).data,
'schema': s.RSFormParseSerializer(schema).data
})
response['Location'] = new_cst.get_absolute_url()
return response
2023-08-23 12:15:16 +03:00
@transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-rename')
def cst_rename(self, request, pk):
''' Rename constituenta possibly changing type. '''
schema = self._get_schema()
2023-08-25 22:51:20 +03:00
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
2023-08-23 12:15:16 +03:00
serializer.is_valid(raise_exception=True)
2023-08-25 22:51:20 +03:00
old_alias = m.Constituenta.objects.get(pk=request.data['id']).alias
2023-08-23 12:15:16 +03:00
serializer.save()
mapping = { old_alias: serializer.validated_data['alias'] }
schema.apply_mapping(mapping, change_aliases=False)
2023-08-25 22:51:20 +03:00
schema.item.refresh_from_db()
cst = m.Constituenta.objects.get(pk=serializer.validated_data['id'])
2023-08-23 12:15:16 +03:00
return Response(status=200, data={
2023-08-25 22:51:20 +03:00
'new_cst': s.ConstituentaSerializer(cst).data,
'schema': s.RSFormParseSerializer(schema).data
2023-08-23 12:15:16 +03:00
})
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
def cst_multidelete(self, request, pk):
''' Endpoint: Delete multiple constituents. '''
2023-07-27 22:04:25 +03:00
schema = self._get_schema()
2023-08-25 22:51:20 +03:00
serializer = s.CstListSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
schema.delete_cst(serializer.validated_data['constituents'])
2023-08-25 22:51:20 +03:00
schema.item.refresh_from_db()
return Response(status=202, data=s.RSFormParseSerializer(schema).data)
@action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request, pk):
''' Endpoint: Move multiple constituents. '''
2023-08-17 21:23:54 +03:00
schema = self._get_schema()
2023-08-25 22:51:20 +03:00
serializer = s.CstMoveSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
2023-08-25 22:51:20 +03:00
schema.item.refresh_from_db()
return Response(status=200, data=s.RSFormParseSerializer(schema).data)
2023-07-27 22:04:25 +03:00
@action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request, pk):
''' Endpoint: Recreate all aliases based on order. '''
2023-07-27 22:04:25 +03:00
schema = self._get_schema()
schema.reset_aliases()
return Response(status=200, data=s.RSFormParseSerializer(schema).data)
2023-07-27 22:04:25 +03:00
@action(detail=True, methods=['patch'], url_path='load-trs')
def load_trs(self, request, pk):
''' Endpoint: Load data from file and replace current schema. '''
2023-08-25 22:51:20 +03:00
serializer = s.RSFormUploadSerializer(data=request.data)
2023-07-27 22:04:25 +03:00
serializer.is_valid(raise_exception=True)
schema = self._get_schema()
load_metadata = serializer.validated_data['load_metadata']
data = utils.read_trs(request.FILES['file'].file)
2023-08-25 22:51:20 +03:00
data['id'] = schema.item.pk
2023-08-25 22:51:20 +03:00
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)
2023-07-15 17:46:19 +03:00
@action(detail=True, methods=['get'])
def contents(self, request, pk):
''' Endpoint: View schema db contents (including constituents). '''
2023-08-25 22:51:20 +03:00
schema = s.RSFormSerializer(self._get_schema()).data
2023-07-15 17:46:19 +03:00
return Response(schema)
@action(detail=True, methods=['get'])
def details(self, request, pk):
''' Endpoint: Detailed schema view including statuses and parse. '''
2023-07-27 22:04:25 +03:00
schema = self._get_schema()
serializer = s.RSFormParseSerializer(schema)
return Response(serializer.data)
2023-07-15 17:46:19 +03:00
@action(detail=True, methods=['post'])
def check(self, request, pk):
''' Endpoint: Check RSLang expression against schema context. '''
2023-08-25 22:51:20 +03:00
serializer = s.ExpressionSerializer(data=request.data)
2023-07-15 17:46:19 +03:00
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
schema = s.PyConceptAdapter(self._get_schema())
result = pyconcept.check_expression(json.dumps(schema.data), expression)
2023-07-15 17:46:19 +03:00
return Response(json.loads(result))
2023-08-23 23:19:43 +03:00
2023-08-23 22:57:25 +03:00
@action(detail=True, methods=['post'])
def resolve(self, request, pk):
''' Endpoint: Resolve refenrces in text against schema terms context. '''
2023-08-25 22:51:20 +03:00
serializer = s.TextSerializer(data=request.data)
2023-08-23 22:57:25 +03:00
serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text']
resolver = self._get_schema().resolver()
2023-08-23 22:57:25 +03:00
resolver.resolve(text)
2023-08-25 22:51:20 +03:00
return Response(status=200, data=s.ResolverSerializer(resolver).data)
2023-07-15 17:46:19 +03:00
@action(detail=True, methods=['get'], url_path='export-trs')
def export_trs(self, request, pk):
''' Endpoint: Download Exteor compatible file. '''
2023-08-25 22:51:20 +03:00
schema = s.RSFormTRSSerializer(self._get_schema()).data
2023-07-15 17:46:19 +03:00
trs = utils.write_trs(schema)
2023-08-25 22:51:20 +03:00
filename = self._get_schema().item.alias
2023-07-15 17:46:19 +03:00
if filename == '' or not filename.isascii():
# Note: non-ascii symbols in Content-Disposition
# are not supported by some browsers
filename = 'Schema'
filename += '.trs'
response = HttpResponse(trs, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={filename}'
return response
class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. '''
2023-08-25 22:51:20 +03:00
serializer_class = s.FileSerializer
2023-07-15 17:46:19 +03:00
def post(self, request):
2023-07-15 17:46:19 +03:00
data = utils.read_trs(request.FILES['file'].file)
owner = self.request.user
if owner.is_anonymous:
owner = None
_prepare_rsform_data(data, request, owner)
2023-08-25 22:51:20 +03:00
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True)
schema = serializer.save()
2023-08-25 22:51:20 +03:00
result = s.LibraryItemSerializer(schema.item)
2023-07-15 17:46:19 +03:00
return Response(status=201, data=result.data)
@api_view(['POST'])
def create_rsform(request):
''' Endpoint: Create RSForm from user input and/or trs file. '''
2023-07-15 17:46:19 +03:00
owner = request.user
if owner.is_anonymous:
owner = None
if 'file' not in request.FILES:
2023-08-25 22:51:20 +03:00
serializer = s.LibraryItemSerializer(data=request.data)
2023-07-15 17:46:19 +03:00
serializer.is_valid(raise_exception=True)
2023-08-25 22:51:20 +03:00
schema = m.RSForm.create(
2023-07-27 22:04:25 +03:00
title=serializer.validated_data['title'],
2023-07-15 17:46:19 +03:00
owner=owner,
2023-07-27 22:04:25 +03:00
alias=serializer.validated_data.get('alias', ''),
comment=serializer.validated_data.get('comment', ''),
is_common=serializer.validated_data.get('is_common', False),
2023-08-25 22:51:20 +03:00
is_canonical=serializer.validated_data.get('is_canonical', False),
2023-07-15 17:46:19 +03:00
)
else:
data = utils.read_trs(request.FILES['file'].file)
_prepare_rsform_data(data, request, owner)
2023-08-25 22:51:20 +03:00
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True)
schema = serializer.save()
2023-08-25 22:51:20 +03:00
result = s.LibraryItemSerializer(schema.item)
2023-07-15 17:46:19 +03:00
return Response(status=201, data=result.data)
2023-08-25 22:51:20 +03:00
def _prepare_rsform_data(data: dict, request, owner: m.User):
data['owner'] = owner
if 'title' in request.data and request.data['title'] != '':
data['title'] = request.data['title']
if data['title'] == '':
data['title'] = 'Без названия ' + request.FILES['file'].fileName
if 'alias' in request.data and request.data['alias'] != '':
data['alias'] = request.data['alias']
if 'comment' in request.data and request.data['comment'] != '':
data['comment'] = request.data['comment']
2023-08-25 22:51:20 +03:00
is_common = True
if 'is_common' in request.data:
is_common = request.data['is_common'] == 'true'
data['is_common'] = is_common
2023-08-25 22:51:20 +03:00
is_canonical = False
if 'is_canonical' in request.data:
is_canonical = request.data['is_canonical'] == 'true'
data['is_canonical'] = is_canonical
2023-07-15 17:46:19 +03:00
@api_view(['POST'])
def parse_expression(request):
''' Endpoint: Parse RS expression. '''
2023-08-25 22:51:20 +03:00
serializer = s.ExpressionSerializer(data=request.data)
2023-07-15 17:46:19 +03:00
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
result = pyconcept.parse_expression(expression)
return Response(json.loads(result))
@api_view(['POST'])
def convert_to_ascii(request):
''' Endpoint: Convert expression to ASCII syntax. '''
2023-08-25 22:51:20 +03:00
serializer = s.ExpressionSerializer(data=request.data)
2023-07-15 17:46:19 +03:00
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
result = pyconcept.convert_to_ascii(expression)
return Response({'result': result})
@api_view(['POST'])
def convert_to_math(request):
''' Endpoint: Convert expression to MATH syntax. '''
2023-08-25 22:51:20 +03:00
serializer = s.ExpressionSerializer(data=request.data)
2023-07-15 17:46:19 +03:00
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
result = pyconcept.convert_to_math(expression)
return Response({'result': result})