mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Implement RSForm upload and fix bugs
This commit is contained in:
parent
467f45b0c8
commit
9dd3155319
|
@ -9,4 +9,4 @@ $exclude = '*/venv/*,*/tests/*,*/migrations/*,*__init__.py,manage.py,apps.py,url
|
|||
& $coverageExec report
|
||||
& $coverageExec html
|
||||
|
||||
Start-Process "file:///D:/DEV/!WORK/Concept-Web/rsconcept/backend/htmlcov/index.html"
|
||||
Start-Process "file:///$PSScriptRoot\backend\htmlcov\index.html"
|
|
@ -11,7 +11,7 @@ def load_initial_schemas(apps, schema_editor):
|
|||
for subdir, dirs, files in os.walk(rootdir):
|
||||
for file in files:
|
||||
data = utils.read_trs(os.path.join(subdir, file))
|
||||
RSForm.import_json(None, data)
|
||||
RSForm.create_from_trs(None, data)
|
||||
|
||||
|
||||
def load_initial_users(apps, schema_editor):
|
||||
|
|
|
@ -98,7 +98,7 @@ class RSForm(models.Model):
|
|||
''' Insert new constituenta at last position '''
|
||||
position = 1
|
||||
if self.constituents().exists():
|
||||
position += self.constituents().only('order').aggregate(models.Max('order'))['order__max']
|
||||
position += self.constituents().count()
|
||||
result = Constituenta.objects.create(
|
||||
schema=self,
|
||||
order=position,
|
||||
|
@ -118,7 +118,7 @@ class RSForm(models.Model):
|
|||
count_bot = 0
|
||||
size = len(listCst)
|
||||
update_list = []
|
||||
for cst in self.constituents():
|
||||
for cst in self.constituents().only('id', 'order').order_by('order'):
|
||||
if cst not in listCst:
|
||||
if count_top + 1 < target:
|
||||
cst.order = count_top + 1
|
||||
|
@ -142,9 +142,38 @@ class RSForm(models.Model):
|
|||
self._update_from_core()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def load_trs(self, data: dict, sync_metadata: bool, skip_update: bool):
|
||||
if sync_metadata:
|
||||
self.title = data.get('title', 'Без названия')
|
||||
self.alias = data.get('alias', '')
|
||||
self.comment = data.get('comment', '')
|
||||
order = 1
|
||||
prev_constituents = self.constituents()
|
||||
loaded_ids = set()
|
||||
for cst_data in data['items']:
|
||||
uid = int(cst_data['entityUID'])
|
||||
if prev_constituents.filter(pk=uid).exists():
|
||||
cst: Constituenta = prev_constituents.get(pk=uid)
|
||||
cst.order = order
|
||||
cst.load_trs(cst_data)
|
||||
cst.save()
|
||||
else:
|
||||
cst = Constituenta.create_from_trs(cst_data, self, order)
|
||||
cst.save()
|
||||
uid = cst.id
|
||||
loaded_ids.add(uid)
|
||||
order += 1
|
||||
for prev_cst in prev_constituents:
|
||||
if prev_cst.id not in loaded_ids:
|
||||
prev_cst.delete()
|
||||
if not skip_update:
|
||||
self._update_from_core()
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def import_json(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
||||
def create_from_trs(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
||||
schema = RSForm.objects.create(
|
||||
title=data.get('title', 'Без названия'),
|
||||
owner=owner,
|
||||
|
@ -152,15 +181,15 @@ class RSForm(models.Model):
|
|||
comment=data.get('comment', ''),
|
||||
is_common=is_common
|
||||
)
|
||||
schema._create_cst_from_json(data['items'])
|
||||
schema._create_items_from_trs(data['items'])
|
||||
return schema
|
||||
|
||||
def to_json(self) -> str:
|
||||
def to_trs(self) -> str:
|
||||
''' Generate JSON string containing all data from RSForm '''
|
||||
result = self._prepare_json_rsform()
|
||||
items = self.constituents().order_by('order')
|
||||
items: list['Constituenta'] = self.constituents().order_by('order')
|
||||
for cst in items:
|
||||
result['items'].append(cst.to_json())
|
||||
result['items'].append(cst.to_trs())
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
|
@ -178,8 +207,9 @@ class RSForm(models.Model):
|
|||
'items': []
|
||||
}
|
||||
|
||||
@transaction.atomic
|
||||
def _update_from_core(self) -> dict:
|
||||
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_json())))
|
||||
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
|
||||
update_list = self.constituents().only('id', 'order')
|
||||
if (len(checked['items']) != update_list.count()):
|
||||
raise ValidationError
|
||||
|
@ -194,10 +224,12 @@ class RSForm(models.Model):
|
|||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
return checked
|
||||
|
||||
def _create_cst_from_json(self, items):
|
||||
@transaction.atomic
|
||||
def _create_items_from_trs(self, items):
|
||||
order = 1
|
||||
for cst in items:
|
||||
Constituenta.import_json(cst, self, order)
|
||||
object = Constituenta.create_from_trs(cst, self, order)
|
||||
object.save()
|
||||
order += 1
|
||||
|
||||
|
||||
|
@ -270,28 +302,43 @@ class Constituenta(models.Model):
|
|||
return self.alias
|
||||
|
||||
@staticmethod
|
||||
def import_json(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
||||
def create_from_trs(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
||||
''' Create constituenta from TRS json '''
|
||||
cst = Constituenta(
|
||||
alias=data['alias'],
|
||||
schema=schema,
|
||||
order=order,
|
||||
cst_type=data['cstType'],
|
||||
convention=data.get('convention', 'Без названия')
|
||||
)
|
||||
if 'definition' in data:
|
||||
if 'formal' in data['definition']:
|
||||
cst.definition_formal = data['definition']['formal']
|
||||
if 'text' in data['definition']:
|
||||
cst.definition_raw = data['definition']['text'].get('raw', '')
|
||||
cst.definition_resolved = data['definition']['text'].get('resolved', '')
|
||||
if 'term' in data:
|
||||
cst.term_raw = data['term'].get('raw', '')
|
||||
cst.term_resolved = data['term'].get('resolved', '')
|
||||
cst.term_forms = data['term'].get('forms', [])
|
||||
cst.save()
|
||||
cst._load_texts(data)
|
||||
return cst
|
||||
|
||||
def to_json(self) -> str:
|
||||
def load_trs(self, data: dict):
|
||||
''' Load data from TRS json '''
|
||||
self.alias = data['alias']
|
||||
self.cst_type = data['cstType']
|
||||
self._load_texts(data)
|
||||
|
||||
def _load_texts(self, data: dict):
|
||||
self.convention = data.get('convention', '')
|
||||
if 'definition' in data:
|
||||
self.definition_formal = data['definition'].get('formal', '')
|
||||
if 'text' in data['definition']:
|
||||
self.definition_raw = data['definition']['text'].get('raw', '')
|
||||
self.definition_resolved = data['definition']['text'].get('resolved', '')
|
||||
else:
|
||||
self.definition_raw = ''
|
||||
self.definition_resolved = ''
|
||||
if 'term' in data:
|
||||
self.term_raw = data['term'].get('raw', '')
|
||||
self.term_resolved = data['term'].get('resolved', '')
|
||||
self.term_forms = data['term'].get('forms', [])
|
||||
else:
|
||||
self.term_raw = ''
|
||||
self.term_resolved = ''
|
||||
self.term_forms = []
|
||||
|
||||
def to_trs(self) -> str:
|
||||
return {
|
||||
'entityUID': self.id,
|
||||
'type': 'constituenta',
|
||||
|
|
|
@ -20,6 +20,23 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = ('owner', 'id')
|
||||
|
||||
|
||||
class RSFormUploadSerializer(serializers.Serializer):
|
||||
file = serializers.FileField()
|
||||
load_metadata = serializers.BooleanField()
|
||||
|
||||
|
||||
class RSFormContentsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RSForm
|
||||
|
||||
def to_representation(self, instance: RSForm):
|
||||
result = RSFormSerializer(instance).data
|
||||
result['items'] = []
|
||||
for cst in instance.constituents().order_by('order'):
|
||||
result['items'].append(ConstituentaSerializer(cst).data)
|
||||
return result
|
||||
|
||||
|
||||
class ConstituentaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Constituenta
|
||||
|
@ -79,7 +96,7 @@ class RSFormDetailsSerlializer(serializers.BaseSerializer):
|
|||
model = RSForm
|
||||
|
||||
def to_representation(self, instance: RSForm):
|
||||
trs = pyconcept.check_schema(json.dumps(instance.to_json()))
|
||||
trs = pyconcept.check_schema(json.dumps(instance.to_trs()))
|
||||
trs = trs.replace('entityUID', 'id')
|
||||
result = json.loads(trs)
|
||||
result['id'] = instance.id
|
||||
|
|
|
@ -209,7 +209,7 @@ class TestRSForm(TestCase):
|
|||
self.assertEqual(x1.order, 2)
|
||||
self.assertEqual(x2.order, 1)
|
||||
|
||||
def test_to_json(self):
|
||||
def test_to_trs(self):
|
||||
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
||||
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
||||
x2 = schema.insert_at(1, 'X2', CstType.BASE)
|
||||
|
@ -223,9 +223,9 @@ class TestRSForm(TestCase):
|
|||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}]}}'
|
||||
)
|
||||
self.assertEqual(schema.to_json(), expected)
|
||||
self.assertEqual(schema.to_trs(), expected)
|
||||
|
||||
def test_import_json(self):
|
||||
def test_create_from_trs(self):
|
||||
input = json.loads(
|
||||
'{"type": "rsform", "title": "Test", "alias": "KS1", '
|
||||
'"comment": "Test", "items": '
|
||||
|
@ -236,7 +236,7 @@ class TestRSForm(TestCase):
|
|||
'"term": {"raw": "", "resolved": ""}, '
|
||||
'"definition": {"formal": "", "text": {"raw": "", "resolved": ""}}}]}'
|
||||
)
|
||||
schema = RSForm.import_json(self.user1, input, False)
|
||||
schema = RSForm.create_from_trs(self.user1, input, False)
|
||||
self.assertEqual(schema.owner, self.user1)
|
||||
self.assertEqual(schema.title, 'Test')
|
||||
self.assertEqual(schema.alias, 'KS1')
|
||||
|
@ -245,3 +245,28 @@ class TestRSForm(TestCase):
|
|||
self.assertEqual(constituents.count(), 2)
|
||||
self.assertEqual(constituents[0].alias, 'X1')
|
||||
self.assertEqual(constituents[0].definition_formal, '123')
|
||||
|
||||
def test_load_trs(self):
|
||||
schema = RSForm.objects.create(title='Test', owner=self.user1, alias='КС1')
|
||||
x2 = schema.insert_last('X2', CstType.BASE)
|
||||
schema.insert_last('X3', CstType.BASE)
|
||||
input = json.loads(
|
||||
'{"title": "Test1", "alias": "KS1", '
|
||||
'"comment": "Test", "items": '
|
||||
'[{"entityUID": "' + str(x2.id) + '", "cstType": "basic", "alias": "X1", "convention": "test", '
|
||||
'"term": {"raw": "t1", "resolved": "t2"}, '
|
||||
'"definition": {"formal": "123", "text": {"raw": "t3", "resolved": "t4"}}}]}'
|
||||
)
|
||||
schema.load_trs(input, sync_metadata=True, skip_update=True)
|
||||
x2.refresh_from_db()
|
||||
self.assertEqual(schema.constituents().count(), 1)
|
||||
self.assertEqual(schema.title, input['title'])
|
||||
self.assertEqual(schema.alias, input['alias'])
|
||||
self.assertEqual(schema.comment, input['comment'])
|
||||
self.assertEqual(x2.alias, input['items'][0]['alias'])
|
||||
self.assertEqual(x2.convention, input['items'][0]['convention'])
|
||||
self.assertEqual(x2.term_raw, input['items'][0]['term']['raw'])
|
||||
self.assertEqual(x2.term_resolved, input['items'][0]['term']['resolved'])
|
||||
self.assertEqual(x2.definition_formal, input['items'][0]['definition']['formal'])
|
||||
self.assertEqual(x2.definition_raw, input['items'][0]['definition']['text']['raw'])
|
||||
self.assertEqual(x2.definition_resolved, input['items'][0]['definition']['text']['resolved'])
|
||||
|
|
|
@ -115,7 +115,6 @@ class TestRSFormViewset(APITestCase):
|
|||
schema.insert_last(alias='X1', type=CstType.BASE)
|
||||
response = self.client.get(f'/api/rsforms/{schema.id}/contents/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, schema.to_json())
|
||||
|
||||
def test_details(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
|
@ -248,6 +247,57 @@ class TestRSFormViewset(APITestCase):
|
|||
data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_reset_aliases(self):
|
||||
schema = self.rsform_owned
|
||||
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['id'], schema.id)
|
||||
|
||||
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=1)
|
||||
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=2)
|
||||
d11 = Constituenta.objects.create(schema=schema, alias='D11', cst_type='term', order=3)
|
||||
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
d11.refresh_from_db()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(x2.alias, 'X1')
|
||||
self.assertEqual(x1.order, 2)
|
||||
self.assertEqual(x1.alias, 'X2')
|
||||
self.assertEqual(d11.order, 3)
|
||||
self.assertEqual(d11.alias, 'D1')
|
||||
|
||||
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_load_trs(self):
|
||||
schema = self.rsform_owned
|
||||
schema.title = 'Testt11'
|
||||
schema.save()
|
||||
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
|
||||
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(f'/api/rsforms/{schema.id}/load-trs/', data=data, format='multipart')
|
||||
schema.refresh_from_db()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(schema.title, 'Testt11')
|
||||
self.assertEqual(len(response.data['items']), 25)
|
||||
self.assertEqual(schema.constituents().count(), 25)
|
||||
self.assertFalse(Constituenta.objects.all().filter(pk=x1.id).exists())
|
||||
|
||||
def test_clone(self):
|
||||
schema = self.rsform_owned
|
||||
schema.title = 'Testt11'
|
||||
schema.save()
|
||||
x1 = Constituenta.objects.create(schema=schema, alias='X12', cst_type='basic', order=1)
|
||||
data = json.dumps({'title': 'Title'})
|
||||
response = self.client.post(f'/api/rsforms/{schema.id}/clone/', data=data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['title'], 'Title')
|
||||
self.assertEqual(response.data['items'][0]['alias'], x1.alias)
|
||||
|
||||
|
||||
class TestFunctionalViews(APITestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -34,6 +34,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
ordering_fields = ('owner', 'title', 'time_update')
|
||||
ordering = ('-time_update')
|
||||
|
||||
def _get_schema(self) -> models.RSForm:
|
||||
return self.get_object()
|
||||
|
||||
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)
|
||||
|
@ -41,10 +44,10 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
return serializer.save()
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ['update', 'destroy', 'partial_update',
|
||||
'cst_create', 'cst_multidelete']:
|
||||
if self.action in ['update', 'destroy', 'partial_update', 'load_trs',
|
||||
'cst_create', 'cst_multidelete', 'reset_aliases']:
|
||||
permission_classes = [utils.ObjectOwnerOrAdmin]
|
||||
elif self.action in ['create', 'claim']:
|
||||
elif self.action in ['create', 'claim', 'clone']:
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
else:
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
@ -53,7 +56,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
@action(detail=True, methods=['post'], url_path='cst-create')
|
||||
def cst_create(self, request, pk):
|
||||
''' Create new constituenta '''
|
||||
schema: models.RSForm = self.get_object()
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.CstCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
if ('insert_after' in serializer.validated_data and serializer.validated_data['insert_after'] is not None):
|
||||
|
@ -74,7 +77,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
|
||||
def cst_multidelete(self, request, pk):
|
||||
''' Delete multiple constituents '''
|
||||
schema: models.RSForm = self.get_object()
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema.delete_cst(serializer.validated_data['constituents'])
|
||||
|
@ -85,7 +88,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
||||
def cst_moveto(self, request, pk):
|
||||
''' Delete multiple constituents '''
|
||||
schema: models.RSForm = self.get_object()
|
||||
schema: models.RSForm = self._get_schema()
|
||||
serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
||||
|
@ -93,9 +96,46 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=200, data=outSerializer.data)
|
||||
|
||||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||
def reset_aliases(self, request, pk):
|
||||
''' Recreate all aliases based on order '''
|
||||
schema = self._get_schema()
|
||||
result = json.loads(pyconcept.reset_aliases(json.dumps(schema.to_trs())))
|
||||
schema.load_trs(data=result, sync_metadata=False, skip_update=True)
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=200, data=outSerializer.data)
|
||||
|
||||
@action(detail=True, methods=['patch'], url_path='load-trs')
|
||||
def load_trs(self, request, pk):
|
||||
''' Load data from file and replace current schema '''
|
||||
serializer = serializers.RSFormUploadSerializer(data=request.data)
|
||||
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)
|
||||
schema.load_trs(data, load_metadata, skip_update=False)
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=200, data=outSerializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='clone')
|
||||
def clone(self, request, pk):
|
||||
''' Clone RSForm constituents and create new schema using new metadata '''
|
||||
serializer = serializers.RSFormSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_schema = models.RSForm.objects.create(
|
||||
title=serializer.validated_data['title'],
|
||||
owner=self.request.user,
|
||||
alias=serializer.validated_data.get('alias', ''),
|
||||
comment=serializer.validated_data.get('comment', ''),
|
||||
is_common=serializer.validated_data.get('is_common', False),
|
||||
)
|
||||
new_schema.load_trs(data=self._get_schema().to_trs(), sync_metadata=False, skip_update=True)
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(new_schema)
|
||||
return Response(status=201, data=outSerializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def claim(self, request, pk=None):
|
||||
schema: models.RSForm = self.get_object()
|
||||
schema = self._get_schema()
|
||||
if schema.owner == self.request.user:
|
||||
return Response(status=304)
|
||||
else:
|
||||
|
@ -105,21 +145,21 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
|
||||
@action(detail=True, methods=['get'])
|
||||
def contents(self, request, pk):
|
||||
''' View schema contents (including constituents) '''
|
||||
schema = self.get_object().to_json()
|
||||
''' View schema db contents (including constituents) '''
|
||||
schema = serializers.RSFormContentsSerializer(self._get_schema()).data
|
||||
return Response(schema)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def details(self, request, pk):
|
||||
''' Detailed schema view including statuses '''
|
||||
schema: models.RSForm = self.get_object()
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def check(self, request, pk):
|
||||
''' Check RS expression against schema context '''
|
||||
schema = self.get_object().to_json()
|
||||
schema = self._get_schema().to_trs()
|
||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
expression = serializer.validated_data['expression']
|
||||
|
@ -129,9 +169,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||
def export_trs(self, request, pk):
|
||||
''' Download Exteor compatible file '''
|
||||
schema = self.get_object().to_json()
|
||||
schema = self._get_schema().to_trs()
|
||||
trs = utils.write_trs(schema)
|
||||
filename = self.get_object().alias
|
||||
filename = self._get_schema().alias
|
||||
if filename == '' or not filename.isascii():
|
||||
# Note: non-ascii symbols in Content-Disposition
|
||||
# are not supported by some browsers
|
||||
|
@ -152,7 +192,7 @@ class TrsImportView(views.APIView):
|
|||
owner = self.request.user
|
||||
if owner.is_anonymous:
|
||||
owner = None
|
||||
schema = models.RSForm.import_json(owner, data)
|
||||
schema = models.RSForm.create_from_trs(owner, data)
|
||||
result = serializers.RSFormSerializer(schema)
|
||||
return Response(status=201, data=result.data)
|
||||
|
||||
|
@ -167,11 +207,11 @@ def create_rsform(request):
|
|||
serializer = serializers.RSFormSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema = models.RSForm.objects.create(
|
||||
title=request.data['title'],
|
||||
title=serializer.validated_data['title'],
|
||||
owner=owner,
|
||||
alias=request.data.get('alias', ''),
|
||||
comment=request.data.get('comment', ''),
|
||||
is_common=request.data.get('is_common', False),
|
||||
alias=serializer.validated_data.get('alias', ''),
|
||||
comment=serializer.validated_data.get('comment', ''),
|
||||
is_common=serializer.validated_data.get('is_common', False),
|
||||
)
|
||||
else:
|
||||
data = utils.read_trs(request.FILES['file'].file)
|
||||
|
@ -186,7 +226,7 @@ def create_rsform(request):
|
|||
is_common = True
|
||||
if ('is_common' in request.data):
|
||||
is_common = request.data['is_common'] == 'true'
|
||||
schema = models.RSForm.import_json(owner, data, is_common)
|
||||
schema = models.RSForm.create_from_trs(owner, data, is_common)
|
||||
result = serializers.RSFormSerializer(schema)
|
||||
return Response(status=201, data=result.data)
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ class LoginSerializer(serializers.Serializer):
|
|||
def validate(self, attrs):
|
||||
username = attrs.get('username')
|
||||
password = attrs.get('password')
|
||||
if username and password:
|
||||
user = authenticate(
|
||||
request=self.context.get('request'),
|
||||
username=username,
|
||||
|
@ -30,9 +29,6 @@ class LoginSerializer(serializers.Serializer):
|
|||
if not user:
|
||||
msg = 'Неправильное сочетание имени пользователя и пароля.'
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
else:
|
||||
msg = 'Заполните оба поля: Имя пользователя и Пароль.'
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
|
||||
|
|
|
@ -29,3 +29,9 @@ class TestLoginSerializer(APITestCase):
|
|||
request = self.factory.post('/users/api/login', data)
|
||||
serializer = LoginSerializer(data=data, context={'request': request})
|
||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
||||
|
||||
def test_validate_empty_username(self):
|
||||
data = {'username': '', 'auth': 'invalid'}
|
||||
request = self.factory.post('/users/api/login', data)
|
||||
serializer = LoginSerializer(data=data, context={'request': request})
|
||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
interface ButtonProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> {
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'> {
|
||||
text?: string
|
||||
icon?: React.ReactNode
|
||||
tooltip?: string
|
||||
|
|
|
@ -5,7 +5,7 @@ import Button from './Button';
|
|||
import Label from './Label';
|
||||
|
||||
interface FileInputProps {
|
||||
id: string
|
||||
id?: string
|
||||
required?: boolean
|
||||
label: string
|
||||
acceptType?: string
|
||||
|
@ -33,7 +33,7 @@ function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onC
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'flex gap-2 py-2 mt-3 items-center ' + widthClass}>
|
||||
<div className={'flex flex-col gap-2 py-2 [&:not(:first-child)]:mt-3 items-start ' + widthClass}>
|
||||
<input id={id} type='file'
|
||||
ref={inputRef}
|
||||
required={required}
|
||||
|
|
20
rsconcept/frontend/src/components/Common/MiniButton.tsx
Normal file
20
rsconcept/frontend/src/components/Common/MiniButton.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
interface MiniButtonProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'title' > {
|
||||
icon?: React.ReactNode
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
function MiniButton({ icon, tooltip, children, ...props }: MiniButtonProps) {
|
||||
return (
|
||||
<button type='button'
|
||||
title={tooltip}
|
||||
className='px-1 py-1 font-bold rounded-full cursor-pointer whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
|
||||
{...props}
|
||||
>
|
||||
{icon && <span>{icon}</span>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default MiniButton;
|
|
@ -39,7 +39,7 @@ function Modal({ title, show, hideWindow, onSubmit, onCancel, canSubmit, childre
|
|||
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
|
||||
</div>
|
||||
<div ref={ref} className='fixed bottom-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit z-[60] clr-card border shadow-md'>
|
||||
{ title && <h1 className='mb-4 text-xl font-bold'>{title}</h1> }
|
||||
{ title && <h1 className='mb-4 text-xl font-bold text-center'>{title}</h1> }
|
||||
<div className='py-2'>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -5,12 +5,12 @@ import { type ErrorInfo } from '../components/BackendError'
|
|||
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
||||
import {
|
||||
type DataCallback, deleteRSForm, getTRSFile,
|
||||
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchRSForm,
|
||||
postClaimRSForm, postNewConstituenta
|
||||
} from '../utils/backendAPI'
|
||||
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchResetAliases,
|
||||
patchRSForm,
|
||||
patchUploadTRS, postClaimRSForm, postNewConstituenta} from '../utils/backendAPI'
|
||||
import {
|
||||
IConstituenta, IConstituentaList, IConstituentaMeta, ICstCreateData,
|
||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormMeta, IRSFormUpdateData
|
||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
||||
} from '../utils/models'
|
||||
import { useAuth } from './AuthContext'
|
||||
|
||||
|
@ -36,9 +36,11 @@ interface IRSFormContext {
|
|||
toggleTracking: () => void
|
||||
|
||||
update: (data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => void
|
||||
destroy: (callback?: DataCallback) => void
|
||||
destroy: (callback?: () => void) => void
|
||||
claim: (callback?: DataCallback<IRSFormMeta>) => void
|
||||
download: (callback: DataCallback<Blob>) => void
|
||||
upload: (data: IRSFormUploadData, callback: () => void) => void
|
||||
resetAliases: (callback: () => void) => void
|
||||
|
||||
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void
|
||||
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void
|
||||
|
@ -114,16 +116,34 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
});
|
||||
}, [schemaID, setError, setSchema, schema])
|
||||
|
||||
const upload = useCallback(
|
||||
(data: IRSFormUploadData, callback?: () => void) => {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
setError(undefined)
|
||||
patchUploadTRS(schemaID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onSuccess: newData => {
|
||||
setSchema(newData);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, setSchema, schema])
|
||||
|
||||
const destroy = useCallback(
|
||||
(callback?: DataCallback) => {
|
||||
(callback?: () => void) => {
|
||||
setError(undefined)
|
||||
deleteRSForm(schemaID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onSuccess: newData => {
|
||||
onSuccess: () => {
|
||||
setSchema(undefined);
|
||||
if (callback) callback(newData);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, setSchema])
|
||||
|
@ -145,6 +165,23 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
});
|
||||
}, [schemaID, setError, schema, user, setSchema])
|
||||
|
||||
const resetAliases = useCallback(
|
||||
(callback?: () => void) => {
|
||||
if (!schema || !user) {
|
||||
return;
|
||||
}
|
||||
setError(undefined)
|
||||
patchResetAliases(schemaID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onSuccess: newData => {
|
||||
setSchema(Object.assign(schema, newData));
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, schema, user, setSchema])
|
||||
|
||||
const download = useCallback(
|
||||
(callback: DataCallback<Blob>) => {
|
||||
setError(undefined)
|
||||
|
@ -218,29 +255,15 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
return (
|
||||
<RSFormContext.Provider value={{
|
||||
schema,
|
||||
error,
|
||||
loading,
|
||||
processing,
|
||||
activeID,
|
||||
activeCst,
|
||||
setActiveID,
|
||||
isForceAdmin,
|
||||
isReadonly,
|
||||
error, loading, processing,
|
||||
activeID, activeCst, setActiveID,
|
||||
isForceAdmin, isReadonly, isOwned, isEditable,
|
||||
isClaimable, isTracking,
|
||||
toggleForceAdmin: () => { setIsForceAdmin(prev => !prev) },
|
||||
toggleReadonly: () => { setIsReadonly(prev => !prev) },
|
||||
isOwned,
|
||||
isEditable,
|
||||
isClaimable,
|
||||
isTracking,
|
||||
toggleTracking,
|
||||
update,
|
||||
download,
|
||||
destroy,
|
||||
claim,
|
||||
cstUpdate,
|
||||
cstCreate,
|
||||
cstDelete,
|
||||
cstMoveTo
|
||||
update, download, upload, destroy, claim, resetAliases,
|
||||
cstUpdate, cstCreate, cstDelete, cstMoveTo
|
||||
}}>
|
||||
{ children }
|
||||
</RSFormContext.Provider>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
/* Transparent button */
|
||||
.clr-btn-clear {
|
||||
@apply hover:bg-gray-300 dark:hover:bg-gray-400
|
||||
@apply hover:bg-gray-300 dark:hover:bg-gray-400 dark:disabled:text-zinc-400 disabled:text-gray-400 text-gray-500 dark:text-zinc-200
|
||||
}
|
||||
|
||||
.clr-checkbox {
|
||||
|
|
|
@ -53,7 +53,7 @@ function RSFormCreatePage() {
|
|||
file: file,
|
||||
fileName: file?.name
|
||||
};
|
||||
void createSchema(data, onSuccess);
|
||||
createSchema(data, onSuccess);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import MiniButton from '../../components/Common/MiniButton';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
import TextArea from '../../components/Common/TextArea';
|
||||
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
||||
|
@ -11,7 +12,7 @@ import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI';
|
|||
import ConstituentsSideList from './ConstituentsSideList';
|
||||
import CreateCstModal from './CreateCstModal';
|
||||
import ExpressionEditor from './ExpressionEditor';
|
||||
import { RSFormTabsList } from './RSFormTabs';
|
||||
import { RSTabsList } from './RSTabs';
|
||||
|
||||
function ConstituentEditor() {
|
||||
const navigate = useNavigate();
|
||||
|
@ -113,7 +114,7 @@ function ConstituentEditor() {
|
|||
insert_after: activeID
|
||||
}
|
||||
cstCreate(data, newCst => {
|
||||
navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${newCst.id}`);
|
||||
navigate(`/rsforms/${schema.id}?tab=${RSTabsList.CST_EDIT}&active=${newCst.id}`);
|
||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||
});
|
||||
}
|
||||
|
@ -167,22 +168,18 @@ function ConstituentEditor() {
|
|||
</span>
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<button type='button'
|
||||
title='Создать конституенты после данной'
|
||||
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
|
||||
<MiniButton
|
||||
tooltip='Создать конституенты после данной'
|
||||
disabled={!isEnabled}
|
||||
onClick={() => { handleAddNew(); }}
|
||||
>
|
||||
<SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />
|
||||
</button>
|
||||
<button type='button'
|
||||
title='Удалить редактируемую конституенту'
|
||||
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
|
||||
icon={<SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Удалить редактируемую конституенту'
|
||||
disabled={!isEnabled}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />
|
||||
</button>
|
||||
icon={<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TextArea id='term' label='Термин'
|
||||
|
|
|
@ -18,13 +18,14 @@ interface ConstituentsTableProps {
|
|||
function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||
const {
|
||||
schema, isEditable,
|
||||
cstCreate, cstDelete, cstMoveTo
|
||||
cstCreate, cstDelete, cstMoveTo, resetAliases
|
||||
} = useRSForm();
|
||||
const { noNavigation } = useConceptTheme();
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||
|
||||
const [showCstModal, setShowCstModal] = useState(false);
|
||||
const [toggledClearRows, setToggledClearRows] = useState(false);
|
||||
|
||||
const handleRowClicked = useCallback(
|
||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||
|
@ -39,7 +40,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
selectedCount: number
|
||||
selectedRows: IConstituenta[]
|
||||
}) => {
|
||||
setSelected(selectedRows.map((cst) => cst.id));
|
||||
setSelected(selectedRows.map(cst => cst.id));
|
||||
}, [setSelected]);
|
||||
|
||||
// Delete selected constituents
|
||||
|
@ -50,8 +51,11 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
const data = {
|
||||
items: selected.map(id => { return { id }; })
|
||||
}
|
||||
const deletedNames = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias).join(', ');
|
||||
cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNames}`));
|
||||
const deletedNames = selected.map(id => schema.items?.find(cst => cst.id === id)?.alias).join(', ');
|
||||
cstDelete(data, () => {
|
||||
toast.success(`Конституенты удалены: ${deletedNames}`);
|
||||
setToggledClearRows(prev => !prev);
|
||||
});
|
||||
}, [selected, schema?.items, cstDelete]);
|
||||
|
||||
// Move selected cst up
|
||||
|
@ -70,7 +74,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
}, -1);
|
||||
const target = Math.max(0, currentIndex - 1) + 1
|
||||
const data = {
|
||||
items: selected.map(id => { return { id }; }),
|
||||
items: selected.map(id => { return { id: id }; }),
|
||||
move_to: target
|
||||
}
|
||||
cstMoveTo(data);
|
||||
|
@ -96,7 +100,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
}, -1);
|
||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
|
||||
const data: ICstMovetoData = {
|
||||
items: selected.map(id => { return { id }; }),
|
||||
items: selected.map(id => { return { id: id }; }),
|
||||
move_to: target
|
||||
}
|
||||
cstMoveTo(data);
|
||||
|
@ -104,8 +108,8 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
|
||||
// Generate new names for all constituents
|
||||
const handleReindex = useCallback(() => {
|
||||
toast.info('Переиндексация');
|
||||
}, []);
|
||||
resetAliases(() => toast.success('Переиндексация конституент успешна'));
|
||||
}, [resetAliases]);
|
||||
|
||||
// Add new constituent
|
||||
const handleAddNew = useCallback((type?: CstType) => {
|
||||
|
@ -130,7 +134,15 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
|
||||
// Implement hotkeys for working with constituents table
|
||||
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (!event.altKey || !isEditable || event.shiftKey) {
|
||||
if (!isEditable) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete' && selected.length > 0) {
|
||||
event.preventDefault();
|
||||
handleDelete();
|
||||
return;
|
||||
}
|
||||
if (!event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
if (processAltKey(event.key)) {
|
||||
|
@ -335,7 +347,11 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
})}
|
||||
</div>}
|
||||
</div>
|
||||
<div className='w-full h-full' onKeyDown={handleTableKey} tabIndex={0}>
|
||||
<div className='w-full h-full'
|
||||
onKeyDown={handleTableKey}
|
||||
tabIndex={0}
|
||||
title='Горячие клавиши:
Двойной клик / Alt + клик - редактирование конституенты
Alt + вверх/вниз - движение конституент
Delete - удаление выбранных
Alt + 1-6, Q,W - добавление конституент'
|
||||
>
|
||||
<DataTableThemed
|
||||
data={schema?.items ?? []}
|
||||
columns={columns}
|
||||
|
@ -356,6 +372,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
onSelectedRowsChange={handleSelectionChange}
|
||||
onRowDoubleClicked={onOpenEdit}
|
||||
onRowClicked={handleRowClicked}
|
||||
clearSelectedRows={toggledClearRows}
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,8 @@ import { useIntl } from 'react-intl';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import Button from '../../components/Common/Button';
|
||||
import Checkbox from '../../components/Common/Checkbox';
|
||||
import MiniButton from '../../components/Common/MiniButton';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
import TextArea from '../../components/Common/TextArea';
|
||||
import TextInput from '../../components/Common/TextInput';
|
||||
|
@ -76,6 +76,32 @@ function RSFormCard() {
|
|||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
|
||||
<div className='relative w-full'>
|
||||
<div className='absolute top-0 right-0'>
|
||||
<MiniButton
|
||||
tooltip='Поделиться схемой'
|
||||
icon={<ShareIcon size={5} color='text-primary'/>}
|
||||
onClick={shareCurrentURLProc}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Скачать TRS файл'
|
||||
icon={<DownloadIcon size={5} color='text-primary'/>}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
||||
icon={<CrownIcon size={5} color={isOwned ? '' : 'text-green'}/>}
|
||||
disabled={!isClaimable || !user}
|
||||
onClick={() => { claimOwnershipProc(claim); }}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Удалить схему'
|
||||
disabled={!isEditable}
|
||||
onClick={handleDelete}
|
||||
icon={<DumpBinIcon size={5} color={isEditable ? 'text-red' : ''} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TextInput id='title' label='Полное название' type='text'
|
||||
required
|
||||
value={title}
|
||||
|
@ -107,33 +133,6 @@ function RSFormCard() {
|
|||
disabled={!isModified || !isEditable}
|
||||
icon={<SaveIcon size={6} />}
|
||||
/>
|
||||
<div className='flex justify-end gap-1'>
|
||||
<Button
|
||||
tooltip='Поделиться схемой'
|
||||
icon={<ShareIcon color='text-primary'/>}
|
||||
onClick={shareCurrentURLProc}
|
||||
/>
|
||||
<Button
|
||||
tooltip='Скачать TRS файл'
|
||||
icon={<DownloadIcon color='text-primary'/>}
|
||||
loading={processing}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
<Button
|
||||
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
||||
icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
|
||||
loading={processing}
|
||||
disabled={!isClaimable || !user}
|
||||
onClick={() => { claimOwnershipProc(claim); }}
|
||||
/>
|
||||
<Button
|
||||
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}
|
||||
icon={<DumpBinIcon color={isEditable ? 'text-red' : ''} />}
|
||||
loading={processing}
|
||||
disabled={!isEditable}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-start mt-2'>
|
||||
|
|
|
@ -11,23 +11,23 @@ import ConstituentEditor from './ConstituentEditor';
|
|||
import ConstituentsTable from './ConstituentsTable';
|
||||
import RSFormCard from './RSFormCard';
|
||||
import RSFormStats from './RSFormStats';
|
||||
import TablistTools from './TablistTools';
|
||||
import RSTabsMenu from './RSTabsMenu';
|
||||
|
||||
export enum RSFormTabsList {
|
||||
export enum RSTabsList {
|
||||
CARD = 0,
|
||||
CST_LIST = 1,
|
||||
CST_EDIT = 2
|
||||
}
|
||||
|
||||
function RSFormTabs() {
|
||||
function RSTabs() {
|
||||
const { setActiveID, activeID, error, schema, loading } = useRSForm();
|
||||
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSFormTabsList.CARD);
|
||||
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD);
|
||||
const [init, setInit] = useState(false);
|
||||
|
||||
const onEditCst = (cst: IConstituenta) => {
|
||||
console.log(`Set active cst: ${cst.alias}`);
|
||||
setActiveID(cst.id);
|
||||
setTabIndex(RSFormTabsList.CST_EDIT)
|
||||
setTabIndex(RSTabsList.CST_EDIT)
|
||||
};
|
||||
|
||||
const onSelectTab = (index: number) => {
|
||||
|
@ -47,7 +47,7 @@ function RSFormTabs() {
|
|||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const tabQuery = url.searchParams.get('tab');
|
||||
setTabIndex(Number(tabQuery) || RSFormTabsList.CARD);
|
||||
setTabIndex(Number(tabQuery) || RSTabsList.CARD);
|
||||
}, [setTabIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -55,7 +55,7 @@ function RSFormTabs() {
|
|||
const url = new URL(window.location.href);
|
||||
const currentActive = url.searchParams.get('active');
|
||||
const currentTab = url.searchParams.get('tab');
|
||||
const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(activeID);
|
||||
const saveHistory = tabIndex === RSTabsList.CST_EDIT && currentActive !== String(activeID);
|
||||
if (currentTab !== String(tabIndex)) {
|
||||
url.searchParams.set('tab', String(tabIndex));
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ function RSFormTabs() {
|
|||
selectedTabClassName='font-bold'
|
||||
>
|
||||
<TabList className='flex items-start w-fit clr-bg-pop'>
|
||||
<TablistTools />
|
||||
<RSTabsMenu />
|
||||
<ConceptTab>Паспорт схемы</ConceptTab>
|
||||
<ConceptTab className='border-x-2 clr-border min-w-[10rem] flex justify-between gap-2'>
|
||||
<span>Конституенты</span>
|
||||
|
@ -112,4 +112,4 @@ function RSFormTabs() {
|
|||
</div>);
|
||||
}
|
||||
|
||||
export default RSFormTabs;
|
||||
export default RSTabs;
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
|
@ -11,8 +11,9 @@ import { useAuth } from '../../context/AuthContext';
|
|||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import useDropdown from '../../hooks/useDropdown';
|
||||
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
|
||||
import UploadRSFormModal from './UploadRSFormModal';
|
||||
|
||||
function TablistTools() {
|
||||
function RSTabsMenu() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
|
@ -23,6 +24,7 @@ function TablistTools() {
|
|||
} = useRSForm();
|
||||
const schemaMenu = useDropdown();
|
||||
const editMenu = useDropdown();
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
|
||||
const handleClaimOwner = useCallback(() => {
|
||||
editMenu.hide();
|
||||
|
@ -41,9 +43,8 @@ function TablistTools() {
|
|||
}, [schemaMenu, download, schema?.alias]);
|
||||
|
||||
const handleUpload = useCallback(() => {
|
||||
// TODO: implement
|
||||
schemaMenu.hide();
|
||||
toast.info('Замена содержимого на файл Экстеора');
|
||||
setShowUploadModal(true);
|
||||
}, [schemaMenu]);
|
||||
|
||||
const handleClone = useCallback(() => {
|
||||
|
@ -58,6 +59,11 @@ function TablistTools() {
|
|||
}, [schemaMenu]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadRSFormModal
|
||||
show={showUploadModal}
|
||||
hideWindow={() => { setShowUploadModal(false); }}
|
||||
/>
|
||||
<div className='flex items-center w-fit'>
|
||||
<div ref={schemaMenu.ref}>
|
||||
<Button
|
||||
|
@ -142,7 +148,8 @@ function TablistTools() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TablistTools
|
||||
export default RSTabsMenu
|
|
@ -0,0 +1,63 @@
|
|||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import Checkbox from '../../components/Common/Checkbox';
|
||||
import FileInput from '../../components/Common/FileInput';
|
||||
import Modal from '../../components/Common/Modal';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import { IRSFormUploadData } from '../../utils/models';
|
||||
|
||||
interface UploadRSFormModalProps {
|
||||
show: boolean
|
||||
hideWindow: () => void
|
||||
}
|
||||
|
||||
function UploadRSFormModal({ show, hideWindow }: UploadRSFormModalProps) {
|
||||
const { upload } = useRSForm();
|
||||
const [loadMetadata, setLoadMetadata] = useState(false);
|
||||
const [file, setFile] = useState<File | undefined>()
|
||||
|
||||
const handleSubmit = () => {
|
||||
hideWindow();
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
const data: IRSFormUploadData = {
|
||||
load_metadata: loadMetadata,
|
||||
file: file,
|
||||
fileName: file.name
|
||||
};
|
||||
upload(data, () => toast.success('Схема загружена из файла'));
|
||||
};
|
||||
|
||||
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
setFile(event.target.files[0]);
|
||||
} else {
|
||||
setFile(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='Загрузка схемы из Экстеор'
|
||||
show={show}
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={!!file}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className='max-w-[20rem]'>
|
||||
<FileInput label='Загрузить файл'
|
||||
acceptType='.trs'
|
||||
onChange={handleFile}
|
||||
/>
|
||||
<Checkbox label='Загружать метаданные'
|
||||
value={loadMetadata}
|
||||
onChange={event => { setLoadMetadata(event.target.checked); }}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadRSFormModal;
|
|
@ -1,13 +1,13 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { RSFormState } from '../../context/RSFormContext';
|
||||
import RSFormTabs from './RSFormTabs';
|
||||
import RSTabs from './RSTabs';
|
||||
|
||||
function RSFormPage() {
|
||||
const { id } = useParams();
|
||||
return (
|
||||
<RSFormState schemaID={id ?? ''}>
|
||||
<RSFormTabs />
|
||||
<RSTabs />
|
||||
</RSFormState>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,12 +8,10 @@ import {
|
|||
ExpressionParse,
|
||||
IConstituentaList,
|
||||
IConstituentaMeta,
|
||||
ICstCreateData,
|
||||
ICstCreatedResponse,
|
||||
ICstMovetoData,
|
||||
ICstUpdateData,
|
||||
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstUpdateData,
|
||||
ICurrentUser, IRSFormCreateData, IRSFormData,
|
||||
IRSFormMeta, IRSFormUpdateData, IUserInfo, IUserLoginData, IUserProfile, IUserSignupData, RSExpression
|
||||
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo,
|
||||
IUserLoginData, IUserProfile, IUserSignupData, RSExpression
|
||||
} from './models'
|
||||
|
||||
// ================ Data transfer types ================
|
||||
|
@ -203,9 +201,30 @@ export function postCheckExpression(schema: string, request: FrontExchange<RSExp
|
|||
});
|
||||
}
|
||||
|
||||
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Reset alias for RSForm id=${target}`,
|
||||
endpoint: `${config.url.BASE}rsforms/${target}/reset-aliases/`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUploadData, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
title: `Replacing data with trs file for RSForm id=${target}`,
|
||||
endpoint: `${config.url.BASE}rsforms/${target}/load-trs/`,
|
||||
request: request,
|
||||
options: {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Helper functions =============
|
||||
function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) {
|
||||
console.log(`[[${title}]] requested`);
|
||||
console.log(`REQUEST: [[${title}]]`);
|
||||
if (request.setLoading) request?.setLoading(true);
|
||||
axios.get<ResponseData>(endpoint, options)
|
||||
.then((response) => {
|
||||
|
@ -222,7 +241,7 @@ function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosReq
|
|||
function AxiosPost<RequestData, ResponseData>(
|
||||
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
||||
) {
|
||||
console.log(`[[${title}]] posted`);
|
||||
console.log(`POST: [[${title}]]`);
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
axios.post<ResponseData>(endpoint, request.data, options)
|
||||
.then((response) => {
|
||||
|
@ -239,7 +258,7 @@ function AxiosPost<RequestData, ResponseData>(
|
|||
function AxiosDelete<RequestData, ResponseData>(
|
||||
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
||||
) {
|
||||
console.log(`[[${title}]] is being deleted`);
|
||||
console.log(`DELETE: [[${title}]]`);
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
axios.delete<ResponseData>(endpoint, options)
|
||||
.then((response) => {
|
||||
|
@ -256,7 +275,7 @@ function AxiosDelete<RequestData, ResponseData>(
|
|||
function AxiosPatch<RequestData, ResponseData>(
|
||||
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
||||
) {
|
||||
console.log(`[[${title}]] is being patrially updated`);
|
||||
console.log(`PATCH: [[${title}]]`);
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
axios.patch<ResponseData>(endpoint, request.data, options)
|
||||
.then((response) => {
|
||||
|
|
|
@ -179,6 +179,12 @@ extends IRSFormUpdateData {
|
|||
fileName?: string
|
||||
}
|
||||
|
||||
export interface IRSFormUploadData {
|
||||
load_metadata: boolean
|
||||
file: File
|
||||
fileName: string
|
||||
}
|
||||
|
||||
// ================ Misc types ================
|
||||
// Constituenta edit mode
|
||||
export enum EditMode {
|
||||
|
|
|
@ -21,7 +21,7 @@ export function claimOwnershipProc(
|
|||
}
|
||||
|
||||
export function deleteRSFormProc(
|
||||
destroy: (callback: DataCallback) => void,
|
||||
destroy: (callback?: () => void) => void,
|
||||
navigate: (path: string) => void
|
||||
) {
|
||||
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user