mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add option to clone only selected constituents
This commit is contained in:
parent
2d707eca72
commit
604578d9da
37
TODO.txt
37
TODO.txt
|
@ -1,28 +1,27 @@
|
||||||
!! This is not complete list of todos !!
|
!! This is not complete list of TODOs !!
|
||||||
This list only contains global tech refactorings and tech debt
|
|
||||||
For more specific TODOs see comments in code
|
For more specific TODOs see comments in code
|
||||||
|
|
||||||
[Functionality]
|
[Functionality - PROGRESS]
|
||||||
- landing page
|
- Landing page
|
||||||
- home page (user specific)
|
- Home page (user specific)
|
||||||
- export PDF
|
|
||||||
- блок нотификаций пользователей
|
|
||||||
- блок синтеза
|
|
||||||
|
|
||||||
- статический анализ схемы
|
- Operational synthesis schema as LibraryItem ?
|
||||||
- конфигурации правил для разных статусов
|
|
||||||
|
|
||||||
- Library organization, search and exploration. Consider new user experience
|
- Draggable rows in constituents table
|
||||||
- поиск по содержимому КС в Библиотеке
|
|
||||||
|
|
||||||
- private projects and permissions. Consider cooperative editing
|
|
||||||
|
|
||||||
- draggable rows in constituents table
|
|
||||||
- Clickable IDs in RSEditor tooltips
|
- Clickable IDs in RSEditor tooltips
|
||||||
|
|
||||||
- ARIA (accessibility considerations) - for now machine reading not supported
|
- Library organization, search and exploration. Consider new user experience
|
||||||
|
- Private projects and permissions. Consider cooperative editing
|
||||||
|
- Rework access setup: project-based, user-based, enable sharing. Prevent enumerating access to private schemas by default
|
||||||
|
|
||||||
- rework access setup: project-based, user-based, enable sharing. Prevent enumerating access to private schemas by default
|
[functionality - PLANNING]
|
||||||
|
- User notifications on edit - consider spam prevention and change aggregation
|
||||||
|
- Static analyzer for RSForm
|
||||||
|
- Content based search in Library
|
||||||
|
|
||||||
|
- Export PDF (Items list, Graph)
|
||||||
|
- ARIA (accessibility considerations) - for now machine reading not supported
|
||||||
|
- Internationalization - at least english version. Consider react.intl
|
||||||
|
|
||||||
|
|
||||||
[Tech]
|
[Tech]
|
||||||
|
@ -41,7 +40,7 @@ For more specific TODOs see comments in code
|
||||||
|
|
||||||
[Research]
|
[Research]
|
||||||
Research and consider integration
|
Research and consider integration
|
||||||
- django-allauth
|
- django-allauth - consider supporting popular auth providers
|
||||||
- drf-messages
|
- drf-messages
|
||||||
|
|
||||||
- radix-ui
|
- radix-ui
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .basics import (
|
||||||
)
|
)
|
||||||
from .data_access import (
|
from .data_access import (
|
||||||
LibraryItemSerializer,
|
LibraryItemSerializer,
|
||||||
|
LibraryItemCloneSerializer,
|
||||||
RSFormSerializer,
|
RSFormSerializer,
|
||||||
RSFormParseSerializer,
|
RSFormParseSerializer,
|
||||||
VersionSerializer,
|
VersionSerializer,
|
||||||
|
|
|
@ -256,6 +256,11 @@ class CstListSerializer(serializers.Serializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryItemCloneSerializer(LibraryItemSerializer):
|
||||||
|
''' Serializer: LibraryItem cloning. '''
|
||||||
|
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
|
||||||
|
|
||||||
|
|
||||||
class CstMoveSerializer(CstListSerializer):
|
class CstMoveSerializer(CstListSerializer):
|
||||||
''' Serializer: Change constituenta position. '''
|
''' Serializer: Change constituenta position. '''
|
||||||
move_to = serializers.IntegerField()
|
move_to = serializers.IntegerField()
|
||||||
|
|
|
@ -80,6 +80,10 @@ class EndpointTester(APITestCase):
|
||||||
response = self.execute(data, **kwargs)
|
response = self.execute(data, **kwargs)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def assertCreated(self, data=None, **kwargs):
|
||||||
|
response = self.execute(data, **kwargs)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def assertBadData(self, data=None, **kwargs):
|
def assertBadData(self, data=None, **kwargs):
|
||||||
response = self.execute(data, **kwargs)
|
response = self.execute(data, **kwargs)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
|
@ -31,6 +31,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
alias='T3',
|
alias='T3',
|
||||||
is_common=True
|
is_common=True
|
||||||
)
|
)
|
||||||
|
self.invalid_item = 1337 + self.common.pk
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/library', method='post')
|
@decl_endpoint('/api/library', method='post')
|
||||||
|
@ -49,6 +50,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
@decl_endpoint('/api/library/{item}', method='patch')
|
@decl_endpoint('/api/library/{item}', method='patch')
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
data = {'id': self.unowned.id, 'title': 'New title'}
|
data = {'id': self.unowned.id, 'title': 'New title'}
|
||||||
|
self.assertNotFound(data, item=self.invalid_item)
|
||||||
self.assertForbidden(data, item=self.unowned.id)
|
self.assertForbidden(data, item=self.unowned.id)
|
||||||
|
|
||||||
data = {'id': self.owned.id, 'title': 'New title'}
|
data = {'id': self.owned.id, 'title': 'New title'}
|
||||||
|
@ -71,6 +73,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}/claim', method='post')
|
@decl_endpoint('/api/library/{item}/claim', method='post')
|
||||||
def test_claim(self):
|
def test_claim(self):
|
||||||
|
self.assertNotFound(item=self.invalid_item)
|
||||||
self.assertForbidden(item=self.owned.id)
|
self.assertForbidden(item=self.owned.id)
|
||||||
|
|
||||||
self.owned.is_common = True
|
self.owned.is_common = True
|
||||||
|
@ -127,6 +130,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}/subscribe', method='post')
|
@decl_endpoint('/api/library/{item}/subscribe', method='post')
|
||||||
def test_subscriptions(self):
|
def test_subscriptions(self):
|
||||||
|
self.assertNotFound(item=self.invalid_item)
|
||||||
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
|
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertFalse(self.user in self.unowned.subscribers())
|
self.assertFalse(self.user in self.unowned.subscribers())
|
||||||
|
@ -174,11 +178,31 @@ class TestLibraryViewset(EndpointTester):
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {'title': 'Title1337'}
|
data = {'title': 'Title1337'}
|
||||||
|
self.assertNotFound(data, item=self.invalid_item)
|
||||||
|
self.assertCreated(data, item=self.unowned.id)
|
||||||
|
|
||||||
|
data = {'title': 'Title1338'}
|
||||||
response = self.execute(data, item=self.owned.id)
|
response = self.execute(data, item=self.owned.id)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
self.assertEqual(len(response.data['items']), 2)
|
||||||
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
||||||
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
|
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
|
||||||
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
||||||
self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw)
|
self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw)
|
||||||
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
|
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
|
||||||
|
|
||||||
|
data = {'title': 'Title1340', 'items': []}
|
||||||
|
response = self.execute(data, item=self.owned.id)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
self.assertEqual(len(response.data['items']), 0)
|
||||||
|
|
||||||
|
data = {'title': 'Title1341', 'items': [x12.pk]}
|
||||||
|
response = self.execute(data, item=self.owned.id)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
self.assertEqual(len(response.data['items']), 1)
|
||||||
|
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
||||||
|
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
|
||||||
|
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
||||||
|
|
|
@ -81,9 +81,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='clone item including contents',
|
summary='clone item including contents',
|
||||||
tags=['Library'],
|
tags=['Library'],
|
||||||
request=s.LibraryItemSerializer,
|
request=s.LibraryItemCloneSerializer,
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_201_CREATED: s.RSFormParseSerializer,
|
c.HTTP_201_CREATED: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -91,7 +93,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['post'], url_path='clone')
|
@action(detail=True, methods=['post'], url_path='clone')
|
||||||
def clone(self, request: Request, pk):
|
def clone(self, request: Request, pk):
|
||||||
''' Endpoint: Create deep copy of library item. '''
|
''' Endpoint: Create deep copy of library item. '''
|
||||||
serializer = s.LibraryItemSerializer(data=request.data)
|
serializer = s.LibraryItemCloneSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
if item.item_type == m.LibraryItemType.RSFORM:
|
if item.item_type == m.LibraryItemType.RSFORM:
|
||||||
|
@ -104,6 +106,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
clone_data['comment'] = serializer.validated_data.get('comment', '')
|
clone_data['comment'] = serializer.validated_data.get('comment', '')
|
||||||
clone_data['is_common'] = serializer.validated_data.get('is_common', False)
|
clone_data['is_common'] = serializer.validated_data.get('is_common', False)
|
||||||
clone_data['is_canonical'] = serializer.validated_data.get('is_canonical', False)
|
clone_data['is_canonical'] = serializer.validated_data.get('is_canonical', False)
|
||||||
|
if 'items' in request.data:
|
||||||
|
filtered_items = []
|
||||||
|
for cst in clone_data['items']:
|
||||||
|
if cst['entityUID'] in request.data['items']:
|
||||||
|
filtered_items.append(cst)
|
||||||
|
clone_data['items'] = filtered_items
|
||||||
|
|
||||||
clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
|
clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
|
||||||
clone.is_valid(raise_exception=True)
|
clone.is_valid(raise_exception=True)
|
||||||
new_schema = clone.save()
|
new_schema = clone.save()
|
||||||
|
@ -111,13 +120,17 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data=s.RSFormParseSerializer(new_schema.item).data
|
data=s.RSFormParseSerializer(new_schema.item).data
|
||||||
)
|
)
|
||||||
return Response(status=c.HTTP_404_NOT_FOUND)
|
return Response(status=c.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='claim item',
|
summary='claim item',
|
||||||
tags=['Library'],
|
tags=['Library'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={c.HTTP_200_OK: s.LibraryItemSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.LibraryItemSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
|
@ -139,7 +152,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
summary='subscribe to item',
|
summary='subscribe to item',
|
||||||
tags=['Library'],
|
tags=['Library'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={c.HTTP_204_NO_CONTENT: None}
|
responses={
|
||||||
|
c.HTTP_204_NO_CONTENT: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def subscribe(self, request: Request, pk):
|
def subscribe(self, request: Request, pk):
|
||||||
|
@ -152,7 +169,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
summary='unsubscribe from item',
|
summary='unsubscribe from item',
|
||||||
tags=['Library'],
|
tags=['Library'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={c.HTTP_204_NO_CONTENT: None},
|
responses={
|
||||||
|
c.HTTP_204_NO_CONTENT: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['delete'])
|
@action(detail=True, methods=['delete'])
|
||||||
def unsubscribe(self, request: Request, pk):
|
def unsubscribe(self, request: Request, pk):
|
||||||
|
|
|
@ -41,7 +41,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='create constituenta',
|
summary='create constituenta',
|
||||||
tags=['Constituenta'],
|
tags=['Constituenta'],
|
||||||
request=s.CstCreateSerializer,
|
request=s.CstCreateSerializer,
|
||||||
responses={c.HTTP_201_CREATED: s.NewCstResponse}
|
responses={
|
||||||
|
c.HTTP_201_CREATED: s.NewCstResponse,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='cst-create')
|
@action(detail=True, methods=['post'], url_path='cst-create')
|
||||||
def cst_create(self, request: Request, pk):
|
def cst_create(self, request: Request, pk):
|
||||||
|
@ -69,7 +73,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='produce the structure of a given constituenta',
|
summary='produce the structure of a given constituenta',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.CstTargetSerializer,
|
request=s.CstTargetSerializer,
|
||||||
responses={c.HTTP_200_OK: s.NewMultiCstResponse}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.NewMultiCstResponse,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-produce-structure')
|
@action(detail=True, methods=['patch'], url_path='cst-produce-structure')
|
||||||
def produce_structure(self, request: Request, pk):
|
def produce_structure(self, request: Request, pk):
|
||||||
|
@ -101,7 +109,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='rename constituenta',
|
summary='rename constituenta',
|
||||||
tags=['Constituenta'],
|
tags=['Constituenta'],
|
||||||
request=s.CstRenameSerializer,
|
request=s.CstRenameSerializer,
|
||||||
responses={c.HTTP_200_OK: s.NewCstResponse}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.NewCstResponse,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-rename')
|
@action(detail=True, methods=['patch'], url_path='cst-rename')
|
||||||
|
@ -135,7 +147,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='substitute constituenta',
|
summary='substitute constituenta',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.CstSubstituteSerializer,
|
request=s.CstSubstituteSerializer,
|
||||||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-substitute')
|
@action(detail=True, methods=['patch'], url_path='cst-substitute')
|
||||||
|
@ -161,7 +177,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='delete constituents',
|
summary='delete constituents',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.CstListSerializer,
|
request=s.CstListSerializer,
|
||||||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-delete-multiple')
|
@action(detail=True, methods=['patch'], url_path='cst-delete-multiple')
|
||||||
def cst_delete_multiple(self, request: Request, pk):
|
def cst_delete_multiple(self, request: Request, pk):
|
||||||
|
@ -183,7 +203,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='move constituenta',
|
summary='move constituenta',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.CstMoveSerializer,
|
request=s.CstMoveSerializer,
|
||||||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
||||||
def cst_moveto(self, request: Request, pk):
|
def cst_moveto(self, request: Request, pk):
|
||||||
|
@ -208,7 +232,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='reset aliases, update expressions and references',
|
summary='reset aliases, update expressions and references',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||||
def reset_aliases(self, request: Request, pk):
|
def reset_aliases(self, request: Request, pk):
|
||||||
|
@ -224,7 +252,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='load data from TRS file',
|
summary='load data from TRS file',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.RSFormUploadSerializer,
|
request=s.RSFormUploadSerializer,
|
||||||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='load-trs')
|
@action(detail=True, methods=['patch'], url_path='load-trs')
|
||||||
def load_trs(self, request: Request, pk):
|
def load_trs(self, request: Request, pk):
|
||||||
|
@ -251,7 +283,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='get all constituents data from DB',
|
summary='get all constituents data from DB',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={c.HTTP_200_OK: s.RSFormSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormSerializer,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def contents(self, request: Request, pk):
|
def contents(self, request: Request, pk):
|
||||||
|
@ -266,7 +301,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='get all constituents data and parses',
|
summary='get all constituents data and parses',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def details(self, request: Request, pk):
|
def details(self, request: Request, pk):
|
||||||
|
@ -281,7 +319,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='check RSLang expression',
|
summary='check RSLang expression',
|
||||||
tags=['RSForm', 'FormalLanguage'],
|
tags=['RSForm', 'FormalLanguage'],
|
||||||
request=s.ExpressionSerializer,
|
request=s.ExpressionSerializer,
|
||||||
responses={c.HTTP_200_OK: s.ExpressionParseSerializer},
|
responses={
|
||||||
|
c.HTTP_200_OK: s.ExpressionParseSerializer,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def check(self, request: Request, pk):
|
def check(self, request: Request, pk):
|
||||||
|
@ -300,7 +341,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='resolve text with references',
|
summary='resolve text with references',
|
||||||
tags=['RSForm', 'NaturalLanguage'],
|
tags=['RSForm', 'NaturalLanguage'],
|
||||||
request=s.TextSerializer,
|
request=s.TextSerializer,
|
||||||
responses={c.HTTP_200_OK: s.ResolverSerializer}
|
responses={
|
||||||
|
c.HTTP_200_OK: s.ResolverSerializer,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def resolve(self, request: Request, pk):
|
def resolve(self, request: Request, pk):
|
||||||
|
@ -319,7 +363,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
summary='export as TRS file',
|
summary='export as TRS file',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=None,
|
request=None,
|
||||||
responses={(c.HTTP_200_OK, 'application/zip'): bytes}
|
responses={
|
||||||
|
(c.HTTP_200_OK, 'application/zip'): bytes,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||||
def export_trs(self, request: Request, pk):
|
def export_trs(self, request: Request, pk):
|
||||||
|
@ -341,7 +388,10 @@ class TrsImportView(views.APIView):
|
||||||
summary='import TRS file into RSForm',
|
summary='import TRS file into RSForm',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.FileSerializer,
|
request=s.FileSerializer,
|
||||||
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer}
|
responses={
|
||||||
|
c.HTTP_201_CREATED: s.LibraryItemSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
def post(self, request: Request):
|
def post(self, request: Request):
|
||||||
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
||||||
|
@ -364,7 +414,10 @@ class TrsImportView(views.APIView):
|
||||||
summary='create new RSForm empty or from file',
|
summary='create new RSForm empty or from file',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.LibraryItemSerializer,
|
request=s.LibraryItemSerializer,
|
||||||
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer}
|
responses={
|
||||||
|
c.HTTP_201_CREATED: s.LibraryItemSerializer,
|
||||||
|
c.HTTP_403_FORBIDDEN: None
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def create_rsform(request: Request):
|
def create_rsform(request: Request):
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ErrorData } from '@/components/info/InfoError';
|
||||||
import { ILibraryItem } from '@/models/library';
|
import { ILibraryItem } from '@/models/library';
|
||||||
import { matchLibraryItem } from '@/models/libraryAPI';
|
import { matchLibraryItem } from '@/models/libraryAPI';
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
import { IRSForm, IRSFormCreateData, IRSFormData } from '@/models/rsform';
|
import { IRSForm, IRSFormCloneData, IRSFormCreateData, IRSFormData } from '@/models/rsform';
|
||||||
import { loadRSFormData } from '@/models/rsformAPI';
|
import { loadRSFormData } from '@/models/rsformAPI';
|
||||||
import {
|
import {
|
||||||
DataCallback,
|
DataCallback,
|
||||||
|
@ -31,7 +31,7 @@ interface ILibraryContext {
|
||||||
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
||||||
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void;
|
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void;
|
||||||
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
|
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
|
||||||
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void;
|
cloneItem: (target: number, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
|
||||||
destroyItem: (target: number, callback?: () => void) => void;
|
destroyItem: (target: number, callback?: () => void) => void;
|
||||||
|
|
||||||
localUpdateItem: (data: ILibraryItem) => void;
|
localUpdateItem: (data: ILibraryItem) => void;
|
||||||
|
@ -194,7 +194,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const cloneItem = useCallback(
|
const cloneItem = useCallback(
|
||||||
(target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
|
(target: number, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
@ -13,43 +13,38 @@ import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { ILibraryItem } from '@/models/library';
|
import { ILibraryItem } from '@/models/library';
|
||||||
import { cloneTitle } from '@/models/libraryAPI';
|
import { cloneTitle } from '@/models/libraryAPI';
|
||||||
import { IRSFormCreateData } from '@/models/rsform';
|
import { ConstituentaID, IRSFormCloneData } from '@/models/rsform';
|
||||||
|
|
||||||
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> {
|
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
base: ILibraryItem;
|
base: ILibraryItem;
|
||||||
|
selected: ConstituentaID[];
|
||||||
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
|
function DlgCloneLibraryItem({ hideWindow, base, selected, totalCount }: DlgCloneLibraryItemProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState(cloneTitle(base));
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState(base.alias);
|
||||||
const [comment, setComment] = useState('');
|
const [comment, setComment] = useState(base.comment);
|
||||||
const [common, setCommon] = useState(false);
|
const [common, setCommon] = useState(base.is_common);
|
||||||
const [canonical, setCanonical] = useState(false);
|
const [onlySelected, setOnlySelected] = useState(false);
|
||||||
|
|
||||||
const { cloneItem } = useLibrary();
|
const { cloneItem } = useLibrary();
|
||||||
|
|
||||||
const canSubmit = useMemo(() => title !== '' && alias !== '', [title, alias]);
|
const canSubmit = useMemo(() => title !== '' && alias !== '', [title, alias]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (base) {
|
|
||||||
setTitle(cloneTitle(base));
|
|
||||||
setAlias(base.alias);
|
|
||||||
setComment(base.comment);
|
|
||||||
setCommon(base.is_common);
|
|
||||||
setCanonical(false);
|
|
||||||
}
|
|
||||||
}, [base, base?.title, base?.alias, base?.comment, base?.is_common]);
|
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
const data: IRSFormCreateData = {
|
const data: IRSFormCloneData = {
|
||||||
item_type: base.item_type,
|
item_type: base.item_type,
|
||||||
title: title,
|
title: title,
|
||||||
alias: alias,
|
alias: alias,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
is_common: common,
|
is_common: common,
|
||||||
is_canonical: canonical
|
is_canonical: false
|
||||||
};
|
};
|
||||||
|
if (onlySelected) {
|
||||||
|
data.items = selected;
|
||||||
|
}
|
||||||
cloneItem(base.id, data, newSchema => {
|
cloneItem(base.id, data, newSchema => {
|
||||||
toast.success(`Копия создана: ${newSchema.alias}`);
|
toast.success(`Копия создана: ${newSchema.alias}`);
|
||||||
router.push(urls.schema(newSchema.id));
|
router.push(urls.schema(newSchema.id));
|
||||||
|
@ -78,11 +73,12 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
|
||||||
className='max-w-sm'
|
className='max-w-sm'
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => setAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea id='dlg_comment' label='Описание' value={comment} onChange={event => setComment(event.target.value)} />
|
||||||
id='dlg_comment'
|
<Checkbox
|
||||||
label='Комментарий'
|
id='dlg_only_selected'
|
||||||
value={comment}
|
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
||||||
onChange={event => setComment(event.target.value)}
|
value={onlySelected}
|
||||||
|
setValue={value => setOnlySelected(value)}
|
||||||
/>
|
/>
|
||||||
<Checkbox id='dlg_is_common' label='Общедоступная схема' value={common} setValue={value => setCommon(value)} />
|
<Checkbox id='dlg_is_common' label='Общедоступная схема' value={common} setValue={value => setCommon(value)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -234,6 +234,13 @@ export interface IRSFormCreateData extends ILibraryUpdateData {
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data, used for cloning {@link IRSForm}.
|
||||||
|
*/
|
||||||
|
export interface IRSFormCloneData extends ILibraryUpdateData {
|
||||||
|
items?: ConstituentaID[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents data, used for uploading {@link IRSForm} as file.
|
* Represents data, used for uploading {@link IRSForm} as file.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -527,7 +527,14 @@ export const RSEditState = ({
|
||||||
{model.schema ? (
|
{model.schema ? (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
||||||
{showClone ? <DlgCloneLibraryItem base={model.schema} hideWindow={() => setShowClone(false)} /> : null}
|
{showClone ? (
|
||||||
|
<DlgCloneLibraryItem
|
||||||
|
base={model.schema}
|
||||||
|
hideWindow={() => setShowClone(false)}
|
||||||
|
selected={selected}
|
||||||
|
totalCount={model.schema.items.length}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{showCreateCst ? (
|
{showCreateCst ? (
|
||||||
<DlgCreateCst
|
<DlgCreateCst
|
||||||
hideWindow={() => setShowCreateCst(false)}
|
hideWindow={() => setShowCreateCst(false)}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
ICstUpdateData,
|
ICstUpdateData,
|
||||||
IInlineSynthesisData,
|
IInlineSynthesisData,
|
||||||
IProduceStructureResponse,
|
IProduceStructureResponse,
|
||||||
|
IRSFormCloneData,
|
||||||
IRSFormCreateData,
|
IRSFormCreateData,
|
||||||
IRSFormData,
|
IRSFormData,
|
||||||
IRSFormUploadData,
|
IRSFormUploadData,
|
||||||
|
@ -212,7 +213,7 @@ export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibrary
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCreateData, IRSFormData>) {
|
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCloneData, IRSFormData>) {
|
||||||
AxiosPost({
|
AxiosPost({
|
||||||
title: 'Clone RSForm',
|
title: 'Clone RSForm',
|
||||||
endpoint: `/api/library/${target}/clone`,
|
endpoint: `/api/library/${target}/clone`,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user