Add OSS creation and fix access policy implementation

This commit is contained in:
IRBorisov 2024-06-03 17:38:30 +03:00
parent 93d56ef4fa
commit 9e02d809a0
23 changed files with 368 additions and 206 deletions

View File

@ -60,3 +60,7 @@ def invalidPosition():
def constituentaNoStructure():
return 'Указанная конституента не обладает теоретико-множественной типизацией'
def missingFile():
return 'Отсутствует прикрепленный файл'

View File

@ -57,6 +57,8 @@ class ItemEditor(ItemOwner):
''' Item permission: Editor or higher. '''
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
if request.user.is_anonymous:
return False
item = _extract_item(obj)
if m.Editor.objects.filter(
item=item,
@ -69,6 +71,9 @@ class ItemEditor(ItemOwner):
class ItemAnyone(ItemEditor):
''' Item permission: Anyone if public. '''
def has_permission(self, request: Request, view: APIView) -> bool:
return True
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
item = _extract_item(obj)
if item.access_policy == m.AccessPolicy.PUBLIC:

View File

@ -20,7 +20,7 @@ from .data_access import (
CstSubstituteSerializer,
CstTargetSerializer,
InlineSynthesisSerializer,
LibraryItemBase,
LibraryItemBaseSerializer,
LibraryItemCloneSerializer,
LibraryItemSerializer,
RSFormParseSerializer,

View File

@ -13,13 +13,13 @@ from .basics import CstParseSerializer
from .io_pyconcept import PyConceptAdapter
class LibraryItemBase(serializers.ModelSerializer):
class LibraryItemBaseSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem entry full access. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('id', 'item_type')
read_only_fields = ('id',)
class LibraryItemSerializer(serializers.ModelSerializer):
@ -31,6 +31,16 @@ class LibraryItemSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy')
class LibraryItemCloneSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem cloning. '''
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
class Meta:
''' serializer metadata. '''
model = LibraryItem
exclude = ['id', 'item_type', 'owner']
class VersionSerializer(serializers.ModelSerializer):
''' Serializer: Version data. '''
class Meta:
@ -220,7 +230,7 @@ class RSFormSerializer(serializers.ModelSerializer):
validated_data=new_cst.validated_data
)
loaded_item = LibraryItemBase(data=data)
loaded_item = LibraryItemBaseSerializer(data=data)
loaded_item.is_valid(raise_exception=True)
loaded_item.update(
instance=cast(LibraryItem, self.instance),
@ -337,11 +347,6 @@ class CstListSerializer(serializers.Serializer):
return attrs
class LibraryItemCloneSerializer(LibraryItemBase):
''' Serializer: LibraryItem cloning. '''
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
class CstMoveSerializer(CstListSerializer):
''' Serializer: Change constituenta position. '''
move_to = serializers.IntegerField()

View File

@ -45,10 +45,28 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library', method='post')
def test_create(self):
data = {'title': 'Title'}
data = {
'title': 'Title',
'alias': 'alias',
}
self.executeBadData(data)
data = {
'item_type': LibraryItemType.OPERATIONS_SCHEMA,
'title': 'Title',
'alias': 'alias',
'access_policy': AccessPolicy.PROTECTED,
'visible': False,
'read_only': True
}
response = self.executeCreated(data)
self.assertEqual(response.data['title'], 'Title')
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], data['item_type'])
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['alias'], data['alias'])
self.assertEqual(response.data['access_policy'], data['access_policy'])
self.assertEqual(response.data['visible'], data['visible'])
self.assertEqual(response.data['read_only'], data['read_only'])
self.logout()
data = {'title': 'Title2'}
@ -273,6 +291,16 @@ class TestLibraryViewset(EndpointTester):
self.assertFalse(response_contains(response, self.unowned))
self.assertFalse(response_contains(response, self.owned))
@decl_endpoint('/api/library', method='get')
def test_library_get(self):
non_schema = LibraryItem.objects.create(
item_type=LibraryItemType.OPERATIONS_SCHEMA,
title='Test4'
)
response = self.executeOK()
self.assertTrue(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned))
self.assertTrue(response_contains(response, self.owned))
@decl_endpoint('/api/library/all', method='get')
def test_retrieve_all(self):

View File

@ -25,27 +25,17 @@ class TestRSFormViewset(EndpointTester):
def setUp(self):
super().setUp()
self.schema = RSForm.create(title='Test', alias='T1', owner=self.user)
self.schema_id = self.schema.item.pk
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.item.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.item.pk
self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.item.pk
@decl_endpoint('/api/rsforms/create-detailed', method='post')
def test_create_rsform_file(self):
work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'title': 'Test123', 'comment': '123', 'alias': 'ks1'}
response = self.client.post(self.endpoint, data=data, format='multipart')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], 'Test123')
self.assertEqual(response.data['alias'], 'ks1')
self.assertEqual(response.data['comment'], '123')
@decl_endpoint('/api/rsforms/create-detailed', method='post')
def test_create_rsform_json(self):
data = {
'title': 'Test123',
'comment': '123',
@ -54,17 +44,20 @@ class TestRSFormViewset(EndpointTester):
'access_policy': AccessPolicy.PROTECTED,
'visible': False
}
response = self.executeCreated(data)
self.executeBadData(data)
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data['file'] = file
response = self.client.post(self.endpoint, data=data, format='multipart')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['alias'], data['alias'])
self.assertEqual(response.data['location'], data['location'])
self.assertEqual(response.data['access_policy'], data['access_policy'])
self.assertEqual(response.data['visible'], data['visible'])
self.assertEqual(response.data['comment'], data['comment'])
@decl_endpoint('/api/rsforms', method='get')
def test_list(self):
def test_list_rsforms(self):
non_schema = LibraryItem.objects.create(
item_type=LibraryItemType.OPERATIONS_SCHEMA,
title='Test3'
@ -72,38 +65,41 @@ class TestRSFormViewset(EndpointTester):
response = self.executeOK()
self.assertFalse(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned.item))
self.assertTrue(response_contains(response, self.schema.item))
response = self.client.get('/api/library')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned.item))
self.assertTrue(response_contains(response, self.schema.item))
self.assertTrue(response_contains(response, self.owned.item))
@decl_endpoint('/api/rsforms/{item}/contents', method='get')
def test_contents(self):
schema = RSForm.create(title='Title1')
schema.insert_new('X1')
self.executeOK(item=schema.item.pk)
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title)
self.assertEqual(response.data['alias'], self.owned.item.alias)
self.assertEqual(response.data['location'], self.owned.item.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.visible)
@decl_endpoint('/api/rsforms/{item}/details', method='get')
def test_details(self):
schema = RSForm.create(title='Test', owner=self.user)
x1 = schema.insert_new(
x1 = self.owned.insert_new(
alias='X1',
term_raw='человек',
term_resolved='человек'
)
x2 = schema.insert_new(
x2 = self.owned.insert_new(
alias='X2',
term_raw='@{X1|plur}',
term_resolved='люди'
)
response = self.executeOK(item=schema.item.pk)
self.assertEqual(response.data['title'], 'Test')
response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title)
self.assertEqual(response.data['alias'], self.owned.item.alias)
self.assertEqual(response.data['location'], self.owned.item.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.visible)
self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['id'], x1.pk)
self.assertEqual(response.data['items'][0]['parse']['status'], 'verified')
@ -115,13 +111,20 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['subscribers'], [self.user.pk])
self.assertEqual(response.data['editors'], [])
self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id)
self.logout()
self.executeOK(item=self.owned_id)
self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/rsforms/{item}/check', method='post')
def test_check(self):
schema = RSForm.create(title='Test')
schema.insert_new('X1')
self.owned.insert_new('X1')
data = {'expression': 'X1=X1'}
response = self.executeOK(data, item=schema.item.pk)
response = self.executeOK(data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], 'math')
self.assertEqual(response.data['astText'], '[=[X1][X1]]')
@ -133,14 +136,13 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/resolve', method='post')
def test_resolve(self):
schema = RSForm.create(title='Test')
x1 = schema.insert_new(
x1 = self.owned.insert_new(
alias='X1',
term_resolved='синий слон'
)
data = {'text': '@{1|редкий} @{X1|plur,datv}'}
response = self.executeOK(data, item=schema.item.pk)
response = self.executeOK(data, item=self.owned_id)
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
self.assertEqual(response.data['output'], 'редким синим слонам')
self.assertEqual(len(response.data['refs']), 2)
@ -190,10 +192,10 @@ class TestRSFormViewset(EndpointTester):
data = {'alias': 'X3', 'cst_type': CstType.BASE}
self.executeForbidden(data, item=self.unowned_id)
self.schema.insert_new('X1')
x2 = self.schema.insert_new('X2')
self.owned.insert_new('X1')
x2 = self.owned.insert_new('X2')
response = self.executeCreated(data, item=self.schema_id)
response = self.executeCreated(data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x3.order, 3)
@ -205,7 +207,7 @@ class TestRSFormViewset(EndpointTester):
'term_raw': 'test',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
}
response = self.executeCreated(data, item=self.schema_id)
response = self.executeCreated(data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x4.order, 3)
@ -215,7 +217,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch')
def test_rename_constituenta(self):
x1 = self.schema.insert_new(
x1 = self.owned.insert_new(
alias='X1',
convention='Test',
term_raw='Test1',
@ -223,7 +225,7 @@ class TestRSFormViewset(EndpointTester):
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}]
)
x2_2 = self.unowned.insert_new('X2')
x3 = self.schema.insert_new(
x3 = self.owned.insert_new(
alias='X3',
term_raw='Test3',
term_resolved='Test3',
@ -233,15 +235,15 @@ class TestRSFormViewset(EndpointTester):
data = {'target': x2_2.pk, 'alias': 'D2', 'cst_type': CstType.TERM}
self.executeForbidden(data, item=self.unowned_id)
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
data = {'target': x1.pk, 'alias': x1.alias, 'cst_type': CstType.TERM}
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
data = {'target': x1.pk, 'alias': x3.alias}
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
d1 = self.schema.insert_new(
d1 = self.owned.insert_new(
alias='D1',
term_raw='@{X1|plur}',
definition_formal='X1'
@ -251,7 +253,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x1.cst_type, CstType.BASE)
data = {'target': x1.pk, 'alias': 'D2', 'cst_type': CstType.TERM}
response = self.executeOK(data, item=self.schema_id)
response = self.executeOK(data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], 'D2')
self.assertEqual(response.data['new_cst']['cst_type'], CstType.TERM)
d1.refresh_from_db()
@ -265,13 +267,13 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_single(self):
x1 = self.schema.insert_new(
x1 = self.owned.insert_new(
alias='X1',
term_raw='Test1',
term_resolved='Test1',
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}]
)
x2 = self.schema.insert_new(
x2 = self.owned.insert_new(
alias='X2',
term_raw='Test2'
)
@ -279,21 +281,21 @@ class TestRSFormViewset(EndpointTester):
data = {'substitutions': [{'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}]}
self.executeForbidden(data, item=self.unowned_id)
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
data = {'substitutions': [{'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}]}
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
data = {'substitutions': [{'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}]}
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
d1 = self.schema.insert_new(
d1 = self.owned.insert_new(
alias='D1',
term_raw='@{X2|sing,datv}',
definition_formal='X1'
)
data = {'substitutions': [{'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}]}
response = self.executeOK(data, item=self.schema_id)
response = self.executeOK(data, item=self.owned_id)
d1.refresh_from_db()
x2.refresh_from_db()
self.assertEqual(x2.term_raw, 'Test1')
@ -302,12 +304,12 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_multiple(self):
self.set_params(item=self.schema_id)
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X2')
d1 = self.schema.insert_new('D1')
d2 = self.schema.insert_new('D2')
d3 = self.schema.insert_new(
self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1')
x2 = self.owned.insert_new('X2')
d1 = self.owned.insert_new('D1')
d2 = self.owned.insert_new('D2')
d3 = self.owned.insert_new(
alias='D3',
definition_formal=r'X1 \ X2'
)
@ -341,7 +343,7 @@ class TestRSFormViewset(EndpointTester):
'transfer_term': True
}
]}
response = self.executeOK(data, item=self.schema_id)
response = self.executeOK(data, item=self.owned_id)
d3.refresh_from_db()
self.assertEqual(d3.definition_formal, r'D1 \ D2')
@ -356,7 +358,7 @@ class TestRSFormViewset(EndpointTester):
'definition_formal': '3',
'definition_raw': '4'
}
response = self.executeCreated(data, item=self.schema_id)
response = self.executeCreated(data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
self.assertEqual(response.data['new_cst']['cst_type'], CstType.BASE)
self.assertEqual(response.data['new_cst']['convention'], '1')
@ -369,43 +371,43 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/cst-delete-multiple', method='patch')
def test_delete_constituenta(self):
self.set_params(item=self.schema_id)
self.set_params(item=self.owned_id)
data = {'items': [1337]}
self.executeBadData(data)
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X2')
x1 = self.owned.insert_new('X1')
x2 = self.owned.insert_new('X2')
data = {'items': [x1.pk]}
response = self.executeOK(data)
x2.refresh_from_db()
self.schema.item.refresh_from_db()
self.owned.item.refresh_from_db()
self.assertEqual(len(response.data['items']), 1)
self.assertEqual(self.schema.constituents().count(), 1)
self.assertEqual(self.owned.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2')
self.assertEqual(x2.order, 1)
x3 = self.unowned.insert_new('X1')
data = {'items': [x3.pk]}
self.executeBadData(data, item=self.schema_id)
self.executeBadData(data, item=self.owned_id)
@decl_endpoint('/api/rsforms/{item}/cst-moveto', method='patch')
def test_move_constituenta(self):
self.set_params(item=self.schema_id)
self.set_params(item=self.owned_id)
data = {'items': [1337], 'move_to': 1}
self.executeBadData(data)
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X2')
x1 = self.owned.insert_new('X1')
x2 = self.owned.insert_new('X2')
data = {'items': [x2.pk], 'move_to': 1}
response = self.executeOK(data)
x1.refresh_from_db()
x2.refresh_from_db()
self.assertEqual(response.data['id'], self.schema_id)
self.assertEqual(response.data['id'], self.owned_id)
self.assertEqual(x1.order, 2)
self.assertEqual(x2.order, 1)
@ -416,14 +418,14 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/reset-aliases', method='patch')
def test_reset_aliases(self):
self.set_params(item=self.schema_id)
self.set_params(item=self.owned_id)
response = self.executeOK()
self.assertEqual(response.data['id'], self.schema_id)
self.assertEqual(response.data['id'], self.owned_id)
x2 = self.schema.insert_new('X2')
x1 = self.schema.insert_new('X1')
d11 = self.schema.insert_new('D11')
x2 = self.owned.insert_new('X2')
x1 = self.owned.insert_new('X1')
d11 = self.owned.insert_new('D11')
response = self.executeOK()
x1.refresh_from_db()
@ -441,43 +443,43 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/load-trs', method='patch')
def test_load_trs(self):
self.set_params(item=self.schema_id)
self.schema.item.title = 'Test11'
self.schema.item.save()
x1 = self.schema.insert_new('X1')
self.set_params(item=self.owned_id)
self.owned.item.title = 'Test11'
self.owned.item.save()
x1 = self.owned.insert_new('X1')
work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False}
response = self.client.patch(self.endpoint, data=data, format='multipart')
self.schema.item.refresh_from_db()
self.owned.item.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.schema.item.title, 'Test11')
self.assertEqual(self.owned.item.title, 'Test11')
self.assertEqual(len(response.data['items']), 25)
self.assertEqual(self.schema.constituents().count(), 25)
self.assertEqual(self.owned.constituents().count(), 25)
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())
@decl_endpoint('/api/rsforms/{item}/cst-produce-structure', method='patch')
def test_produce_structure(self):
self.set_params(item=self.schema_id)
x1 = self.schema.insert_new('X1')
s1 = self.schema.insert_new(
self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1')
s1 = self.owned.insert_new(
alias='S1',
definition_formal='(X1×X1)'
)
s2 = self.schema.insert_new(
s2 = self.owned.insert_new(
alias='S2',
definition_formal='invalid'
)
s3 = self.schema.insert_new(
s3 = self.owned.insert_new(
alias='S3',
definition_formal='X1×(X1×(X1))×(X1×X1)'
)
a1 = self.schema.insert_new(
a1 = self.owned.insert_new(
alias='A1',
definition_formal='1=1'
)
f1 = self.schema.insert_new(
f1 = self.owned.insert_new(
alias='F10',
definition_formal='[α∈X1, β∈X1] Fi1[{α,β}](S1)'
)

View File

@ -18,74 +18,22 @@ from .. import permissions
from .. import serializers as s
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of library items available for active user. '''
permission_classes = (permissions.Anyone,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return m.LibraryItem.objects.filter(
Q(access_policy=m.AccessPolicy.PUBLIC),
).filter(
Q(location__startswith=m.LocationHead.COMMON) |
Q(location__startswith=m.LocationHead.LIBRARY)
).order_by('-time_update')
else:
user = cast(m.User, self.request.user)
# pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter(
(
Q(access_policy=m.AccessPolicy.PUBLIC) &
(
Q(location__startswith=m.LocationHead.COMMON) |
Q(location__startswith=m.LocationHead.LIBRARY)
)
) |
Q(owner=user) |
Q(editor__editor=user) |
Q(subscription__user=user)
).distinct().order_by('-time_update')
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryAdminView(generics.ListAPIView):
''' Endpoint: Get list of all library items. Admin only '''
permission_classes = (permissions.GlobalAdmin,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
return m.LibraryItem.objects.all().order_by('-time_update')
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryTemplatesView(generics.ListAPIView):
''' Endpoint: Get list of templates. '''
permission_classes = (permissions.Anyone,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
template_ids = m.LibraryTemplate.objects.values_list('lib_source', flat=True)
return m.LibraryItem.objects.filter(pk__in=template_ids)
# pylint: disable=too-many-ancestors
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryViewSet(viewsets.ModelViewSet):
''' Endpoint: Library operations. '''
queryset = m.LibraryItem.objects.all()
serializer_class = s.LibraryItemSerializer
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_fields = ['item_type', 'owner']
ordering_fields = ('item_type', 'owner', 'alias', 'title', 'time_update')
ordering = '-time_update'
def get_serializer_class(self):
if self.action == 'create':
return s.LibraryItemBaseSerializer
return s.LibraryItemSerializer
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)
@ -103,7 +51,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']:
permission_list = [permissions.GlobalUser]
else:
permission_list = [permissions.Anyone]
permission_list = [permissions.ItemAnyone]
return [permission() for permission in permission_list]
def _get_item(self) -> m.LibraryItem:
@ -308,3 +256,58 @@ class LibraryViewSet(viewsets.ModelViewSet):
editors = serializer.validated_data['users']
m.Editor.set(item=item, users=editors)
return Response(status=c.HTTP_200_OK)
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of library items available for active user. '''
permission_classes = (permissions.Anyone,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return m.LibraryItem.objects.filter(
Q(access_policy=m.AccessPolicy.PUBLIC),
).filter(
Q(location__startswith=m.LocationHead.COMMON) |
Q(location__startswith=m.LocationHead.LIBRARY)
).order_by('-time_update')
else:
user = cast(m.User, self.request.user)
# pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter(
(
Q(access_policy=m.AccessPolicy.PUBLIC) &
(
Q(location__startswith=m.LocationHead.COMMON) |
Q(location__startswith=m.LocationHead.LIBRARY)
)
) |
Q(owner=user) |
Q(editor__editor=user) |
Q(subscription__user=user)
).distinct().order_by('-time_update')
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryAdminView(generics.ListAPIView):
''' Endpoint: Get list of all library items. Admin only '''
permission_classes = (permissions.GlobalAdmin,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
return m.LibraryItem.objects.all().order_by('-time_update')
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryTemplatesView(generics.ListAPIView):
''' Endpoint: Get list of templates. '''
permission_classes = (permissions.Anyone,)
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
template_ids = m.LibraryTemplate.objects.values_list('lib_source', flat=True)
return m.LibraryItem.objects.filter(pk__in=template_ids)

View File

@ -441,6 +441,7 @@ class TrsImportView(views.APIView):
request=s.LibraryItemSerializer,
responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None
}
)
@ -449,17 +450,9 @@ def create_rsform(request: Request):
''' Endpoint: Create RSForm from user input and/or trs file. '''
owner = cast(m.User, request.user) if not request.user.is_anonymous else None
if 'file' not in request.FILES:
serializer = s.LibraryItemBase(data=request.data)
serializer.is_valid(raise_exception=True)
schema = m.RSForm.create(
title=serializer.validated_data['title'],
owner=owner,
alias=serializer.validated_data.get('alias', ''),
comment=serializer.validated_data.get('comment', ''),
visible=serializer.validated_data.get('visible', True),
read_only=serializer.validated_data.get('read_only', False),
access_policy=serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC),
location=serializer.validated_data.get('location', m.LocationHead.USER),
return Response(
status=c.HTTP_400_BAD_REQUEST,
data={f'file': msg.missingFile()}
)
else:
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)

View File

@ -1,6 +1,6 @@
import { createBrowserRouter } from 'react-router-dom';
import CreateItemPage from '@/pages/CreateRSFormPage';
import CreateItemPage from '@/pages/CreateItemPage';
import HomePage from '@/pages/HomePage';
import LibraryPage from '@/pages/LibraryPage';
import LoginPage from '@/pages/LoginPage';

View File

@ -198,7 +198,7 @@ export function getTemplates(request: FrontPull<ILibraryItem[]>) {
});
}
export function postNewRSForm(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
export function postRSFormFromFile(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/rsforms/create-detailed',
request: request,
@ -210,6 +210,13 @@ export function postNewRSForm(request: FrontExchange<ILibraryCreateData, ILibrar
});
}
export function postCreateLibraryItem(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/library',
request: request
});
}
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCloneData, IRSFormData>) {
AxiosPost({
endpoint: `/api/library/${target}/clone`,

View File

@ -44,6 +44,7 @@ export const urls = {
help_topic: (topic: string) => `/manuals?topic=${topic}`,
schema: (id: number | string, version?: number | string) =>
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
oss: (id: number | string) => `/oss/${id}`,
schema_props: ({ id, tab, version, active }: SchemaProps) => {
const versionStr = version !== undefined ? `v=${version}&` : '';
const activeStr = active !== undefined ? `&active=${active}` : '';

View File

@ -1,4 +1,4 @@
import { AccessPolicy, LocationHead } from '@/models/library';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
import {
@ -13,10 +13,12 @@ import {
IconGraphInputs,
IconGraphOutputs,
IconHide,
IconOSS,
IconPrivate,
IconProps,
IconProtected,
IconPublic,
IconRSForm,
IconSettings,
IconShow,
IconTemplates,
@ -29,6 +31,15 @@ export interface DomIconProps<RequestData> extends IconProps {
value: RequestData;
}
export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
switch (value) {
case LibraryItemType.RSFORM:
return <IconRSForm size={size} className={className ?? 'clr-text-primary'} />;
case LibraryItemType.OSS:
return <IconOSS size={size} className={className ?? 'clr-text-green'} />;
}
}
export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
switch (value) {
case AccessPolicy.PRIVATE:

View File

@ -56,6 +56,8 @@ export { TbBriefcase as IconBusiness } from 'react-icons/tb';
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { FaRegObjectGroup as IconOSS } from 'react-icons/fa';
export { RiHexagonLine as IconRSForm } from 'react-icons/ri';
export { LuArchive as IconArchive } from 'react-icons/lu';
export { LuDatabase as IconDatabase } from 'react-icons/lu';
export { LuImage as IconImage } from 'react-icons/lu';

View File

@ -0,0 +1,62 @@
'use client';
import { useCallback } from 'react';
import Dropdown from '@/components/ui/Dropdown';
import useDropdown from '@/hooks/useDropdown';
import { LibraryItemType } from '@/models/library';
import { prefixes } from '@/utils/constants';
import { describeLibraryItemType, labelLibraryItemType } from '@/utils/labels';
import { ItemTypeIcon } from '../DomainIcons';
import DropdownButton from '../ui/DropdownButton';
import SelectorButton from '../ui/SelectorButton';
interface SelectItemTypeProps {
value: LibraryItemType;
onChange: (value: LibraryItemType) => void;
disabled?: boolean;
stretchLeft?: boolean;
}
function SelectItemType({ value, disabled, stretchLeft, onChange }: SelectItemTypeProps) {
const menu = useDropdown();
const handleChange = useCallback(
(newValue: LibraryItemType) => {
menu.hide();
if (newValue !== value) {
onChange(newValue);
}
},
[menu, value, onChange]
);
return (
<div ref={menu.ref}>
<SelectorButton
transparent
title={describeLibraryItemType(value)}
hideTitle={menu.isOpen}
className='h-full py-1 px-2 disabled:cursor-auto rounded-lg'
icon={<ItemTypeIcon value={value} size='1.25rem' />}
text={labelLibraryItemType(value)}
onClick={menu.toggle}
disabled={disabled}
/>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft}>
{Object.values(LibraryItemType).map((item, index) => (
<DropdownButton
key={`${prefixes.policy_list}${index}`}
text={labelLibraryItemType(item)}
title={describeLibraryItemType(item)}
icon={<ItemTypeIcon value={item} size='1rem' />}
onClick={() => handleChange(item)}
/>
))}
</Dropdown>
</div>
);
}
export default SelectItemType;

View File

@ -34,7 +34,6 @@ function SelectMatchMode({ value, onChange }: SelectMatchModeProps) {
<div ref={menu.ref}>
<SelectorButton
transparent
tabIndex={-1}
title='Настройка фильтрации по проверяемым атрибутам'
hideTitle={menu.isOpen}
className='h-full pr-2'

View File

@ -26,6 +26,7 @@ function SelectorButton({
return (
<button
type='button'
tabIndex={-1}
className={clsx(
'px-1 flex flex-start items-center gap-1',
'text-sm font-controls select-none',

View File

@ -10,7 +10,8 @@ import {
getRSFormDetails,
getTemplates,
postCloneLibraryItem,
postNewRSForm
postCreateLibraryItem,
postRSFormFromFile
} from '@/app/backendAPI';
import { ErrorData } from '@/components/info/InfoError';
import { ILibraryItem, LibraryItemID } from '@/models/library';
@ -181,20 +182,31 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
const createItem = useCallback(
(data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => {
const onSuccess = (newSchema: ILibraryItem) =>
reloadItems(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema);
});
setError(undefined);
postNewRSForm({
data: data,
showError: true,
setLoading: setProcessing,
onError: setError,
onSuccess: newSchema =>
reloadItems(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema);
})
});
if (data.file) {
postRSFormFromFile({
data: data,
showError: true,
setLoading: setProcessing,
onError: setError,
onSuccess: onSuccess
});
} else {
postCreateLibraryItem({
data: data,
showError: true,
setLoading: setProcessing,
onError: setError,
onSuccess: onSuccess
});
}
},
[reloadItems, user]
);

View File

@ -9,7 +9,7 @@ import { UserID } from './user';
*/
export enum LibraryItemType {
RSFORM = 'rsform',
OPERATIONS_SCHEMA = 'oss'
OSS = 'oss'
}
/**

View File

@ -9,6 +9,7 @@ import { VisibilityIcon } from '@/components/DomainIcons';
import { IconDownload } from '@/components/Icons';
import InfoError from '@/components/info/InfoError';
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
import SelectItemType from '@/components/select/SelectItemType';
import SelectLocationHead from '@/components/select/SelectLocationHead';
import Button from '@/components/ui/Button';
import Label from '@/components/ui/Label';
@ -30,6 +31,7 @@ function FormCreateItem() {
const { user } = useAuth();
const { createItem, error, setError, processing } = useLibrary();
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
const [title, setTitle] = useState('');
const [alias, setAlias] = useState('');
const [comment, setComment] = useState('');
@ -64,7 +66,7 @@ function FormCreateItem() {
return;
}
const data: ILibraryCreateData = {
item_type: LibraryItemType.RSFORM,
item_type: itemType,
title: title,
alias: alias,
comment: comment,
@ -75,9 +77,13 @@ function FormCreateItem() {
file: file,
fileName: file?.name
};
createItem(data, newSchema => {
createItem(data, newItem => {
toast.success('Схема успешно создана');
router.push(urls.schema(newSchema.id));
if (itemType == LibraryItemType.RSFORM) {
router.push(urls.schema(newItem.id));
} else {
router.push(urls.oss(newItem.id));
}
});
}
@ -109,8 +115,8 @@ function FormCreateItem() {
/>
</Overlay>
<form className={clsx('cc-column', 'min-w-[30rem]', 'px-6 py-3')} onSubmit={handleSubmit}>
<h1>Создание концептуальной схемы</h1>
<form className={clsx('cc-column', 'min-w-[30rem] max-w-[30rem]', 'px-6 py-3')} onSubmit={handleSubmit}>
<h1>Создание схемы</h1>
{fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null}
@ -135,11 +141,14 @@ function FormCreateItem() {
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<div className='flex flex-col items-center gap-2'>
<Label text='Тип схемы' className='self-center select-none' />
<SelectItemType value={itemType} onChange={setItemType} />
</div>
<div className='flex flex-col gap-2'>
<Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'>
<SelectAccessPolicy value={policy} onChange={newPolicy => setPolicy(newPolicy)} />
<SelectAccessPolicy value={policy} onChange={setPolicy} />
<MiniButton
className='disabled:cursor-auto'
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}

View File

@ -2,7 +2,6 @@
import clsx from 'clsx';
import Divider from '@/components/ui/Divider';
import FlexColumn from '@/components/ui/FlexColumn';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext';
@ -52,9 +51,6 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row')}>
<FlexColumn className='px-3'>
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<Divider margins='my-1' />
<EditorLibraryItem item={schema} isModified={isModified} />
</FlexColumn>

View File

@ -6,7 +6,7 @@
*/
import { GraphLayout } from '@/components/ui/GraphUI';
import { GramData, Grammeme, ReferenceType } from '@/models/language';
import { AccessPolicy, LocationHead } from '@/models/library';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
import {
@ -821,6 +821,28 @@ export function describeAccessPolicy(policy: AccessPolicy): string {
}
}
/**
* Retrieves label for {@link LibraryItemType}.
*/
export function labelLibraryItemType(itemType: LibraryItemType): string {
// prettier-ignore
switch (itemType) {
case LibraryItemType.RSFORM: return 'КС';
case LibraryItemType.OSS: return 'ОСС';
}
}
/**
* Retrieves description for {@link LibraryItemType}.
*/
export function describeLibraryItemType(itemType: LibraryItemType): string {
// prettier-ignore
switch (itemType) {
case LibraryItemType.RSFORM: return 'Концептуальная схема';
case LibraryItemType.OSS: return 'Операционная схема синтеза';
}
}
/**
* UI shared messages.
*/