From 9dd31553193e32cd6394649e63c1ebb6523fc952 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Thu, 27 Jul 2023 22:04:25 +0300 Subject: [PATCH] Implement RSForm upload and fix bugs --- rsconcept/RunCoverage.ps1 | 2 +- .../rsform/migrations/0002_load_commons.py | 2 +- rsconcept/backend/apps/rsform/models.py | 95 ++++++++++++++----- rsconcept/backend/apps/rsform/serializers.py | 19 +++- .../backend/apps/rsform/tests/t_models.py | 33 ++++++- .../backend/apps/rsform/tests/t_views.py | 52 +++++++++- rsconcept/backend/apps/rsform/views.py | 78 +++++++++++---- rsconcept/backend/apps/users/serializers.py | 18 ++-- .../backend/apps/users/tests/t_serializers.py | 6 ++ .../frontend/src/components/Common/Button.tsx | 2 +- .../src/components/Common/FileInput.tsx | 4 +- .../src/components/Common/MiniButton.tsx | 20 ++++ .../frontend/src/components/Common/Modal.tsx | 2 +- .../frontend/src/context/RSFormContext.tsx | 79 +++++++++------ rsconcept/frontend/src/index.css | 2 +- .../frontend/src/pages/RSFormCreatePage.tsx | 2 +- .../pages/RSFormPage/ConstituentEditor.tsx | 25 +++-- .../pages/RSFormPage/ConstituentsTable.tsx | 41 +++++--- .../src/pages/RSFormPage/RSFormCard.tsx | 55 ++++++----- .../RSFormPage/{RSFormTabs.tsx => RSTabs.tsx} | 18 ++-- .../{TablistTools.tsx => RSTabsMenu.tsx} | 17 +++- .../pages/RSFormPage/UploadRSFormModal.tsx | 63 ++++++++++++ .../frontend/src/pages/RSFormPage/index.tsx | 4 +- rsconcept/frontend/src/utils/backendAPI.ts | 37 ++++++-- rsconcept/frontend/src/utils/models.ts | 6 ++ rsconcept/frontend/src/utils/procedures.ts | 2 +- 26 files changed, 508 insertions(+), 176 deletions(-) create mode 100644 rsconcept/frontend/src/components/Common/MiniButton.tsx rename rsconcept/frontend/src/pages/RSFormPage/{RSFormTabs.tsx => RSTabs.tsx} (90%) rename rsconcept/frontend/src/pages/RSFormPage/{TablistTools.tsx => RSTabsMenu.tsx} (93%) create mode 100644 rsconcept/frontend/src/pages/RSFormPage/UploadRSFormModal.tsx diff --git a/rsconcept/RunCoverage.ps1 b/rsconcept/RunCoverage.ps1 index a66c9d85..15dc7461 100644 --- a/rsconcept/RunCoverage.ps1 +++ b/rsconcept/RunCoverage.ps1 @@ -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" \ No newline at end of file +Start-Process "file:///$PSScriptRoot\backend\htmlcov\index.html" \ No newline at end of file diff --git a/rsconcept/backend/apps/rsform/migrations/0002_load_commons.py b/rsconcept/backend/apps/rsform/migrations/0002_load_commons.py index 9696b8db..e96a6c46 100644 --- a/rsconcept/backend/apps/rsform/migrations/0002_load_commons.py +++ b/rsconcept/backend/apps/rsform/migrations/0002_load_commons.py @@ -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): diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index cbcbe0c6..3b672aa8 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -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', diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index 3425a7cb..b71add1b 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -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 diff --git a/rsconcept/backend/apps/rsform/tests/t_models.py b/rsconcept/backend/apps/rsform/tests/t_models.py index df2c78fa..6b76370b 100644 --- a/rsconcept/backend/apps/rsform/tests/t_models.py +++ b/rsconcept/backend/apps/rsform/tests/t_models.py @@ -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']) diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index 137eab3e..328bfddc 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -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): diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 92e8a430..05327850 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -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) diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 7b2567fe..cd81331b 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -21,17 +21,13 @@ 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, - password=password - ) - if not user: - msg = 'Неправильное сочетание имени пользователя и пароля.' - raise serializers.ValidationError(msg, code='authorization') - else: - msg = 'Заполните оба поля: Имя пользователя и Пароль.' + user = authenticate( + request=self.context.get('request'), + username=username, + password=password + ) + if not user: + msg = 'Неправильное сочетание имени пользователя и пароля.' raise serializers.ValidationError(msg, code='authorization') attrs['user'] = user return attrs diff --git a/rsconcept/backend/apps/users/tests/t_serializers.py b/rsconcept/backend/apps/users/tests/t_serializers.py index 48feb84c..e677559c 100644 --- a/rsconcept/backend/apps/users/tests/t_serializers.py +++ b/rsconcept/backend/apps/users/tests/t_serializers.py @@ -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)) diff --git a/rsconcept/frontend/src/components/Common/Button.tsx b/rsconcept/frontend/src/components/Common/Button.tsx index 9e7a5d54..90d5b03f 100644 --- a/rsconcept/frontend/src/components/Common/Button.tsx +++ b/rsconcept/frontend/src/components/Common/Button.tsx @@ -1,5 +1,5 @@ interface ButtonProps -extends Omit, 'className' | 'children'> { +extends Omit, 'className' | 'children' | 'title'> { text?: string icon?: React.ReactNode tooltip?: string diff --git a/rsconcept/frontend/src/components/Common/FileInput.tsx b/rsconcept/frontend/src/components/Common/FileInput.tsx index f2f07486..fcf590cf 100644 --- a/rsconcept/frontend/src/components/Common/FileInput.tsx +++ b/rsconcept/frontend/src/components/Common/FileInput.tsx @@ -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 ( -
+
, 'className' | 'title' > { + icon?: React.ReactNode + tooltip?: string +} + +function MiniButton({ icon, tooltip, children, ...props }: MiniButtonProps) { + return ( + + ); +} + +export default MiniButton; diff --git a/rsconcept/frontend/src/components/Common/Modal.tsx b/rsconcept/frontend/src/components/Common/Modal.tsx index 31ea2249..2eaef2aa 100644 --- a/rsconcept/frontend/src/components/Common/Modal.tsx +++ b/rsconcept/frontend/src/components/Common/Modal.tsx @@ -39,7 +39,7 @@ function Modal({ title, show, hideWindow, onSubmit, onCancel, canSubmit, childre
- { title &&

{title}

} + { title &&

{title}

}
{children}
diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 7645d2a3..58fcee70 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -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) => void - destroy: (callback?: DataCallback) => void + destroy: (callback?: () => void) => void claim: (callback?: DataCallback) => void download: (callback: DataCallback) => void + upload: (data: IRSFormUploadData, callback: () => void) => void + resetAliases: (callback: () => void) => void cstCreate: (data: ICstCreateData, callback?: DataCallback) => void cstUpdate: (data: ICstUpdateData, callback?: DataCallback) => void @@ -113,17 +115,35 @@ 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) => { setError(undefined) @@ -218,29 +255,15 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { return ( { 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 } diff --git a/rsconcept/frontend/src/index.css b/rsconcept/frontend/src/index.css index 4cc42148..030b7489 100644 --- a/rsconcept/frontend/src/index.css +++ b/rsconcept/frontend/src/index.css @@ -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 { diff --git a/rsconcept/frontend/src/pages/RSFormCreatePage.tsx b/rsconcept/frontend/src/pages/RSFormCreatePage.tsx index 1f35310b..ed7e52c5 100644 --- a/rsconcept/frontend/src/pages/RSFormCreatePage.tsx +++ b/rsconcept/frontend/src/pages/RSFormCreatePage.tsx @@ -53,7 +53,7 @@ function RSFormCreatePage() { file: file, fileName: file?.name }; - void createSchema(data, onSuccess); + createSchema(data, onSuccess); }; return ( diff --git a/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx b/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx index b9b5736d..0c8a30bd 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx @@ -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() {
- - + icon={} + />