mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-15 05:10:36 +03:00
Implement backend for versions
This commit is contained in:
parent
b2ada0d630
commit
ce945711e2
|
@ -130,11 +130,20 @@ class ExpressionParseSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class VersionSerializer(serializers.ModelSerializer):
|
class VersionSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Version data. '''
|
''' Serializer: Version data. '''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = Version
|
||||||
|
fields = 'id', 'version', 'item', 'description', 'time_create'
|
||||||
|
read_only_fields = ('id', 'item', 'time_create')
|
||||||
|
|
||||||
|
|
||||||
|
class VersionInnerSerializer(serializers.ModelSerializer):
|
||||||
|
''' Serializer: Version data for list of versions. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Version
|
model = Version
|
||||||
fields = 'id', 'version', 'description', 'time_create'
|
fields = 'id', 'version', 'description', 'time_create'
|
||||||
read_only_fields = ('item', 'id', 'time_create')
|
read_only_fields = ('id', 'item', 'time_create')
|
||||||
|
|
||||||
|
|
||||||
class VersionCreateSerializer(serializers.ModelSerializer):
|
class VersionCreateSerializer(serializers.ModelSerializer):
|
||||||
|
@ -160,7 +169,7 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
||||||
return [item.pk for item in instance.subscribers()]
|
return [item.pk for item in instance.subscribers()]
|
||||||
|
|
||||||
def get_versions(self, instance: LibraryItem) -> list:
|
def get_versions(self, instance: LibraryItem) -> list:
|
||||||
return [VersionSerializer(item).data for item in instance.versions()]
|
return [VersionInnerSerializer(item).data for item in instance.versions()]
|
||||||
|
|
||||||
|
|
||||||
class ConstituentaSerializer(serializers.ModelSerializer):
|
class ConstituentaSerializer(serializers.ModelSerializer):
|
||||||
|
@ -429,7 +438,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def to_versioned_data(self) -> dict:
|
def to_versioned_data(self) -> dict:
|
||||||
''' Create serializable version representation without redundant data. '''
|
''' Create serializable version representation without redundant data. '''
|
||||||
result = self.to_representation(self.instance)
|
result = self.to_representation(cast(LibraryItem, self.instance))
|
||||||
del result['versions']
|
del result['versions']
|
||||||
del result['subscribers']
|
del result['subscribers']
|
||||||
|
|
||||||
|
@ -442,7 +451,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def from_versioned_data(self, version: int, data: dict) -> dict:
|
def from_versioned_data(self, version: int, data: dict) -> dict:
|
||||||
''' Load data from version. '''
|
''' Load data from version. '''
|
||||||
result = self.to_representation(self.instance)
|
result = self.to_representation(cast(LibraryItem, self.instance))
|
||||||
result['version'] = version
|
result['version'] = version
|
||||||
return result | data
|
return result | data
|
||||||
|
|
||||||
|
@ -651,3 +660,8 @@ class NewCstResponse(serializers.Serializer):
|
||||||
''' Serializer: Create cst response. '''
|
''' Serializer: Create cst response. '''
|
||||||
new_cst = ConstituentaSerializer()
|
new_cst = ConstituentaSerializer()
|
||||||
schema = RSFormParseSerializer()
|
schema = RSFormParseSerializer()
|
||||||
|
|
||||||
|
class NewVersionResponse(serializers.Serializer):
|
||||||
|
''' Serializer: Create cst response. '''
|
||||||
|
version = serializers.IntegerField()
|
||||||
|
schema = RSFormParseSerializer()
|
||||||
|
|
|
@ -828,6 +828,86 @@ class TestVersionViews(APITestCase):
|
||||||
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieve_version(self):
|
||||||
|
data = {'version': '1.0.0', 'description': 'test'}
|
||||||
|
response = self.client.post(
|
||||||
|
f'/api/rsforms/{self.owned.item.id}/versions/create',
|
||||||
|
data=data, format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
version_id = response.data['version']
|
||||||
|
|
||||||
|
invalid_id = 1338
|
||||||
|
response = self.client.get(f'/api/rsforms/{invalid_id}/versions/{invalid_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
response = self.client.get(f'/api/rsforms/{self.owned.item.id}/versions/{invalid_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
response = self.client.get(f'/api/rsforms/{invalid_id}/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
response = self.client.get(f'/api/rsforms/{self.unowned.item.id}/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
self.owned.item.alias = 'NewName'
|
||||||
|
self.owned.item.save()
|
||||||
|
self.x1.alias = 'X33'
|
||||||
|
self.x1.save()
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/rsforms/{self.owned.item.id}/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertNotEqual(response.data['alias'], self.owned.item.alias)
|
||||||
|
self.assertNotEqual(response.data['items'][0]['alias'], self.x1.alias)
|
||||||
|
self.assertEqual(response.data['version'], version_id)
|
||||||
|
|
||||||
|
def test_access_version(self):
|
||||||
|
data = {'version': '1.0.0', 'description': 'test'}
|
||||||
|
response = self.client.post(
|
||||||
|
f'/api/rsforms/{self.owned.item.id}/versions/create',
|
||||||
|
data=data, format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
version_id = response.data['version']
|
||||||
|
invalid_id = version_id + 1337
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/versions/{invalid_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get(f'/api/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['version'], data['version'])
|
||||||
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
self.assertEqual(response.data['item'], self.owned.item.id)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f'/api/versions/{version_id}',
|
||||||
|
data=data, format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
response = self.client.delete(f'/api/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
data = {'version': '1.1.0', 'description': 'test1'}
|
||||||
|
response = self.client.patch(
|
||||||
|
f'/api/versions/{version_id}',
|
||||||
|
data=data, format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['version'], data['version'])
|
||||||
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
||||||
|
response = self.client.delete(f'/api/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/versions/{version_id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
class TestRSLanguageViews(APITestCase):
|
class TestRSLanguageViews(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.factory = APIRequestFactory()
|
self.factory = APIRequestFactory()
|
||||||
|
|
|
@ -9,11 +9,14 @@ library_router.register('rsforms', views.RSFormViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('library/active', views.LibraryActiveView.as_view(), name='library'),
|
path('library/active', views.LibraryActiveView.as_view(), name='library'),
|
||||||
path('library/templates', views.LibraryTemplatesView.as_view(), name='library'),
|
path('library/templates', views.LibraryTemplatesView.as_view(), name='templates'),
|
||||||
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
|
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
|
||||||
path('rsforms/import-trs', views.TrsImportView.as_view()),
|
path('rsforms/import-trs', views.TrsImportView.as_view()),
|
||||||
path('rsforms/create-detailed', views.create_rsform),
|
path('rsforms/create-detailed', views.create_rsform),
|
||||||
|
|
||||||
|
path('versions/<int:pk>', views.VersionAPIView.as_view()),
|
||||||
path('rsforms/<int:pk_item>/versions/create', views.create_version),
|
path('rsforms/<int:pk_item>/versions/create', views.create_version),
|
||||||
|
path('rsforms/<int:pk_item>/versions/<int:pk_version>', views.retrieve_version),
|
||||||
|
|
||||||
path('rslang/parse-expression', views.parse_expression),
|
path('rslang/parse-expression', views.parse_expression),
|
||||||
path('rslang/to-ascii', views.convert_to_ascii),
|
path('rslang/to-ascii', views.convert_to_ascii),
|
||||||
|
|
|
@ -34,6 +34,15 @@ class SchemaOwnerOrAdmin(BasePermission):
|
||||||
return request.user.is_staff # type: ignore
|
return request.user.is_staff # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class ItemOwnerOrAdmin(BasePermission):
|
||||||
|
''' Permission for object ownership restriction '''
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if request.user == obj.item.owner:
|
||||||
|
return True
|
||||||
|
if not hasattr(request.user, 'is_staff'):
|
||||||
|
return False
|
||||||
|
return request.user.is_staff # type: ignore
|
||||||
|
|
||||||
def read_trs(file) -> dict:
|
def read_trs(file) -> dict:
|
||||||
''' Read JSON from TRS file '''
|
''' Read JSON from TRS file '''
|
||||||
with ZipFile(file, 'r') as archive:
|
with ZipFile(file, 'r') as archive:
|
||||||
|
@ -51,7 +60,7 @@ def write_trs(json_data: dict) -> bytes:
|
||||||
return content.getvalue()
|
return content.getvalue()
|
||||||
|
|
||||||
def apply_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str:
|
def apply_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str:
|
||||||
''' Apply mapping to matching in regular expression patter subgroup 1. '''
|
''' Apply mapping to matching in regular expression patter subgroup 1 '''
|
||||||
if text == '' or pattern == '':
|
if text == '' or pattern == '':
|
||||||
return text
|
return text
|
||||||
pos_input: int = 0
|
pos_input: int = 0
|
||||||
|
|
|
@ -58,13 +58,29 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView):
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
result = super().get_permissions()
|
result = super().get_permissions()
|
||||||
if self.request.method.lower() == 'get':
|
if self.request.method.upper() == 'GET':
|
||||||
result.append(permissions.AllowAny())
|
result.append(permissions.AllowAny())
|
||||||
else:
|
else:
|
||||||
result.append(utils.SchemaOwnerOrAdmin())
|
result.append(utils.SchemaOwnerOrAdmin())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=['Version'])
|
||||||
|
@extend_schema_view()
|
||||||
|
class VersionAPIView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
''' Endpoint: Get / Update Constituenta. '''
|
||||||
|
queryset = m.Version.objects.all()
|
||||||
|
serializer_class = s.VersionSerializer
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
result = super().get_permissions()
|
||||||
|
if self.request.method.upper() == 'GET':
|
||||||
|
result.append(permissions.AllowAny())
|
||||||
|
else:
|
||||||
|
result.append(utils.ItemOwnerOrAdmin())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
# pylint: disable=too-many-ancestors
|
||||||
@extend_schema(tags=['Library'])
|
@extend_schema(tags=['Library'])
|
||||||
@extend_schema_view()
|
@extend_schema_view()
|
||||||
|
@ -196,10 +212,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
''' Determine permission class. '''
|
''' Determine permission class. '''
|
||||||
if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple',
|
if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple',
|
||||||
'reset_aliases', 'cst_rename', 'cst_substitute']:
|
'reset_aliases', 'cst_rename', 'cst_substitute']:
|
||||||
permission_classes = [utils.ObjectOwnerOrAdmin]
|
permission_list = [utils.ObjectOwnerOrAdmin]
|
||||||
else:
|
else:
|
||||||
permission_classes = [permissions.AllowAny]
|
permission_list = [permissions.AllowAny]
|
||||||
return [permission() for permission in permission_classes]
|
return [permission() for permission in permission_list]
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='create constituenta',
|
summary='create constituenta',
|
||||||
|
@ -547,10 +563,10 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='save version for RSForm copying current content',
|
summary='save version for RSForm copying current content',
|
||||||
tags=['Versions'],
|
tags=['Version'],
|
||||||
request=s.VersionCreateSerializer,
|
request=s.VersionCreateSerializer,
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_201_CREATED: s.RSFormParseSerializer,
|
c.HTTP_201_CREATED: s.NewVersionResponse,
|
||||||
c.HTTP_403_FORBIDDEN: None,
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
}
|
}
|
||||||
|
@ -584,6 +600,36 @@ def create_version(request: Request, pk_item: int):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='retrieve versioned data for RSForm',
|
||||||
|
tags=['Version'],
|
||||||
|
request=None,
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api_view(['GET'])
|
||||||
|
def retrieve_version(request: Request, pk_item: int, pk_version: int):
|
||||||
|
''' Endpoint: Retrieve version for RSForm. '''
|
||||||
|
try:
|
||||||
|
item = m.LibraryItem.objects.get(pk=pk_item)
|
||||||
|
except m.LibraryItem.DoesNotExist:
|
||||||
|
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||||
|
try:
|
||||||
|
version = m.Version.objects.get(pk=pk_version)
|
||||||
|
except m.Version.DoesNotExist:
|
||||||
|
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||||
|
if version.item != item:
|
||||||
|
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
data = s.RSFormSerializer(item).from_versioned_data(version.pk, version.data)
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_200_OK,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='RS expression into Syntax Tree',
|
summary='RS expression into Syntax Tree',
|
||||||
tags=['FormalLanguage'],
|
tags=['FormalLanguage'],
|
||||||
|
|
Loading…
Reference in New Issue
Block a user