Refactor backend using drf-spectacular

This commit is contained in:
IRBorisov 2023-09-21 23:09:51 +03:00
parent c322e2e8eb
commit f21a01bbc0
15 changed files with 445 additions and 133 deletions

View File

@ -58,6 +58,7 @@ This readme file is used mostly to document project dependencies
- djangorestframework - djangorestframework
- django-cors-headers - django-cors-headers
- django-filter - django-filter
- drf-spectacular
- tzdata - tzdata
- gunicorn - gunicorn
- coreapi - coreapi
@ -72,7 +73,6 @@ This readme file is used mostly to document project dependencies
- coverage - coverage
- pylint - pylint
- mypy - mypy
- django-stubs[compatible-mypy]
- djangorestframework-stubs[compatible-mypy] - djangorestframework-stubs[compatible-mypy]
</pre> </pre>
</details> </details>

View File

@ -27,6 +27,11 @@ class ExpressionSerializer(serializers.Serializer):
expression = serializers.CharField() expression = serializers.CharField()
class ResultTextSerializer(serializers.Serializer):
''' Serializer: Text result of a function call. '''
result = serializers.CharField()
class TextSerializer(serializers.Serializer): class TextSerializer(serializers.Serializer):
''' Serializer: Text with references. ''' ''' Serializer: Text with references. '''
text = serializers.CharField() text = serializers.CharField()
@ -379,6 +384,7 @@ class CstRenameSerializer(serializers.ModelSerializer):
class CstListSerializer(serializers.Serializer): class CstListSerializer(serializers.Serializer):
''' Serializer: List of constituents from one origin. ''' ''' Serializer: List of constituents from one origin. '''
# TODO: fix schema
items = serializers.ListField( items = serializers.ListField(
child=CstStandaloneSerializer() child=CstStandaloneSerializer()
) )
@ -403,6 +409,7 @@ class CstMoveSerializer(CstListSerializer):
class ResolverSerializer(serializers.Serializer): class ResolverSerializer(serializers.Serializer):
''' Serializer: Resolver results serializer. ''' ''' Serializer: Resolver results serializer. '''
# TODO: add schema
def to_representation(self, instance: Resolver) -> dict: def to_representation(self, instance: Resolver) -> dict:
return { return {
'input': instance.input, 'input': instance.input,

View File

@ -9,6 +9,7 @@ from rest_framework import views, viewsets, filters, generics, permissions
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from drf_spectacular.utils import extend_schema, extend_schema_view
import pyconcept import pyconcept
from . import models as m from . import models as m
@ -16,6 +17,8 @@ from . import serializers as s
from . import utils from . import utils
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryActiveView(generics.ListAPIView): class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of rsforms available for active user. ''' ''' Endpoint: Get list of rsforms available for active user. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -32,6 +35,8 @@ class LibraryActiveView(generics.ListAPIView):
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update') return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
@extend_schema(tags=['Constituenta'])
@extend_schema_view()
class ConstituentAPIView(generics.RetrieveUpdateAPIView): class ConstituentAPIView(generics.RetrieveUpdateAPIView):
''' Endpoint: Get / Update Constituenta. ''' ''' Endpoint: Get / Update Constituenta. '''
queryset = m.Constituenta.objects.all() queryset = m.Constituenta.objects.all()
@ -45,7 +50,10 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView):
result.append(utils.SchemaOwnerOrAdmin()) result.append(utils.SchemaOwnerOrAdmin())
return result return result
# pylint: disable=too-many-ancestors # pylint: disable=too-many-ancestors
@extend_schema(tags=['Library'])
@extend_schema_view()
class LibraryViewSet(viewsets.ModelViewSet): class LibraryViewSet(viewsets.ModelViewSet):
''' Endpoint: Library operations. ''' ''' Endpoint: Library operations. '''
queryset = m.LibraryItem.objects.all() queryset = m.LibraryItem.objects.all()
@ -76,6 +84,12 @@ class LibraryViewSet(viewsets.ModelViewSet):
def _get_item(self) -> m.LibraryItem: def _get_item(self) -> m.LibraryItem:
return cast(m.LibraryItem, self.get_object()) return cast(m.LibraryItem, self.get_object())
# TODO: response schema
@extend_schema(
request=s.LibraryItemSerializer,
summary='clone item including contents',
tags=['Library']
)
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['post'], url_path='clone') @action(detail=True, methods=['post'], url_path='clone')
def clone(self, request, pk): def clone(self, request, pk):
@ -99,6 +113,12 @@ class LibraryViewSet(viewsets.ModelViewSet):
return Response(status=201, data=s.RSFormParseSerializer(new_schema).data) return Response(status=201, data=s.RSFormParseSerializer(new_schema).data)
return Response(status=404) return Response(status=404)
@extend_schema(
request=None,
responses={200: s.LibraryItemSerializer},
summary='claim item',
tags=['Library']
)
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def claim(self, request, pk=None): def claim(self, request, pk=None):
@ -112,6 +132,12 @@ class LibraryViewSet(viewsets.ModelViewSet):
m.Subscription.subscribe(user=item.owner, item=item) m.Subscription.subscribe(user=item.owner, item=item)
return Response(status=200, data=s.LibraryItemSerializer(item).data) return Response(status=200, data=s.LibraryItemSerializer(item).data)
@extend_schema(
request=None,
responses={200: None},
summary='subscribe to item',
tags=['Library']
)
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def subscribe(self, request, pk): def subscribe(self, request, pk):
''' Endpoint: Subscribe current user to item. ''' ''' Endpoint: Subscribe current user to item. '''
@ -119,6 +145,12 @@ class LibraryViewSet(viewsets.ModelViewSet):
m.Subscription.subscribe(user=self.request.user, item=item) m.Subscription.subscribe(user=self.request.user, item=item)
return Response(status=200) return Response(status=200)
@extend_schema(
request=None,
responses={200: None},
summary='unsubscribe from item',
tags=['Library']
)
@action(detail=True, methods=['delete']) @action(detail=True, methods=['delete'])
def unsubscribe(self, request, pk): def unsubscribe(self, request, pk):
''' Endpoint: Unsubscribe current user from item. ''' ''' Endpoint: Unsubscribe current user from item. '''
@ -127,6 +159,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
return Response(status=200) return Response(status=200)
@extend_schema(tags=['RSForm'])
@extend_schema_view()
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. ''' ''' Endpoint: RSForm operations. '''
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM) queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM)
@ -144,6 +178,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes] return [permission() for permission in permission_classes]
# TODO: response schema
@extend_schema(
request=s.CstCreateSerializer,
summary='create constituenta',
tags=['Constituenta']
)
@action(detail=True, methods=['post'], url_path='cst-create') @action(detail=True, methods=['post'], url_path='cst-create')
def cst_create(self, request, pk): def cst_create(self, request, pk):
''' Create new constituenta. ''' ''' Create new constituenta. '''
@ -160,6 +200,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
response['Location'] = new_cst.get_absolute_url() response['Location'] = new_cst.get_absolute_url()
return response return response
# TODO: response schema
@extend_schema(
request=s.CstRenameSerializer,
summary='rename constituenta',
tags=['Constituenta']
)
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-rename') @action(detail=True, methods=['patch'], url_path='cst-rename')
def cst_rename(self, request, pk): def cst_rename(self, request, pk):
@ -178,6 +224,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'schema': s.RSFormParseSerializer(schema).data 'schema': s.RSFormParseSerializer(schema).data
}) })
# TODO: response schema
@extend_schema(
request=s.CstListSerializer,
summary='delete constituents',
tags=['Constituenta']
)
@action(detail=True, methods=['patch'], url_path='cst-multidelete') @action(detail=True, methods=['patch'], url_path='cst-multidelete')
def cst_multidelete(self, request, pk): def cst_multidelete(self, request, pk):
''' Endpoint: Delete multiple constituents. ''' ''' Endpoint: Delete multiple constituents. '''
@ -188,6 +240,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.item.refresh_from_db() schema.item.refresh_from_db()
return Response(status=202, data=s.RSFormParseSerializer(schema).data) return Response(status=202, data=s.RSFormParseSerializer(schema).data)
# TODO: response schema
@extend_schema(
request=s.CstMoveSerializer,
summary='move constituenta',
tags=['Constituenta']
)
@action(detail=True, methods=['patch'], url_path='cst-moveto') @action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request, pk): def cst_moveto(self, request, pk):
''' Endpoint: Move multiple constituents. ''' ''' Endpoint: Move multiple constituents. '''
@ -198,6 +256,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.item.refresh_from_db() schema.item.refresh_from_db()
return Response(status=200, data=s.RSFormParseSerializer(schema).data) return Response(status=200, data=s.RSFormParseSerializer(schema).data)
# TODO: response schema
@extend_schema(
request=None,
summary='reset aliases, update expressions and references',
tags=['RSForm']
)
@action(detail=True, methods=['patch'], url_path='reset-aliases') @action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request, pk): def reset_aliases(self, request, pk):
''' Endpoint: Recreate all aliases based on order. ''' ''' Endpoint: Recreate all aliases based on order. '''
@ -205,6 +269,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.reset_aliases() schema.reset_aliases()
return Response(status=200, data=s.RSFormParseSerializer(schema).data) return Response(status=200, data=s.RSFormParseSerializer(schema).data)
# TODO: response schema
@extend_schema(
request=s.RSFormUploadSerializer,
summary='load data from TRS file',
tags=['RSForm']
)
@action(detail=True, methods=['patch'], url_path='load-trs') @action(detail=True, methods=['patch'], url_path='load-trs')
def load_trs(self, request, pk): def load_trs(self, request, pk):
''' Endpoint: Load data from file and replace current schema. ''' ''' Endpoint: Load data from file and replace current schema. '''
@ -220,12 +290,24 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = serializer.save() schema = serializer.save()
return Response(status=200, data=s.RSFormParseSerializer(schema).data) return Response(status=200, data=s.RSFormParseSerializer(schema).data)
# TODO: response schema
@extend_schema(
request=None,
summary='get all constituents data from DB',
tags=['RSForm']
)
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def contents(self, request, pk): def contents(self, request, pk):
''' Endpoint: View schema db contents (including constituents). ''' ''' Endpoint: View schema db contents (including constituents). '''
schema = s.RSFormSerializer(self._get_schema()).data schema = s.RSFormSerializer(self._get_schema()).data
return Response(schema) return Response(schema)
# TODO: response schema
@extend_schema(
request=None,
summary='get all constituents data and parses',
tags=['RSForm']
)
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def details(self, request, pk): def details(self, request, pk):
''' Endpoint: Detailed schema view including statuses and parse. ''' ''' Endpoint: Detailed schema view including statuses and parse. '''
@ -233,6 +315,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.RSFormParseSerializer(schema) serializer = s.RSFormParseSerializer(schema)
return Response(serializer.data) return Response(serializer.data)
# TODO: response schema
@extend_schema(
request=s.ExpressionSerializer,
summary='check RSLang expression',
tags=['RSForm', 'Functions']
)
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def check(self, request, pk): def check(self, request, pk):
''' Endpoint: Check RSLang expression against schema context. ''' ''' Endpoint: Check RSLang expression against schema context. '''
@ -243,6 +331,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
result = pyconcept.check_expression(json.dumps(schema.data), expression) result = pyconcept.check_expression(json.dumps(schema.data), expression)
return Response(json.loads(result)) return Response(json.loads(result))
@extend_schema(
request=s.TextSerializer,
responses={200: s.ResolverSerializer},
summary='resolve text with references',
tags=['RSForm', 'Functions']
)
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def resolve(self, request, pk): def resolve(self, request, pk):
''' Endpoint: Resolve refenrces in text against schema terms context. ''' ''' Endpoint: Resolve refenrces in text against schema terms context. '''
@ -253,6 +347,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
resolver.resolve(text) resolver.resolve(text)
return Response(status=200, data=s.ResolverSerializer(resolver).data) return Response(status=200, data=s.ResolverSerializer(resolver).data)
# TODO: create a proper file response schema
@extend_schema(
responses={(200, 'application/zip'): bytes},
summary='export as TRS file',
tags=['RSForm']
)
@action(detail=True, methods=['get'], url_path='export-trs') @action(detail=True, methods=['get'], url_path='export-trs')
def export_trs(self, request, pk): def export_trs(self, request, pk):
''' Endpoint: Download Exteor compatible file. ''' ''' Endpoint: Download Exteor compatible file. '''
@ -274,6 +374,10 @@ class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. ''' ''' Endpoint: Upload RS form in Exteor format. '''
serializer_class = s.FileSerializer serializer_class = s.FileSerializer
@extend_schema(
summary='import TRS file into RSForm',
tags=['RSForm']
)
def post(self, request): def post(self, request):
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
owner = self.request.user owner = self.request.user
@ -287,6 +391,10 @@ class TrsImportView(views.APIView):
return Response(status=201, data=result.data) return Response(status=201, data=result.data)
@extend_schema(
summary='create new RSForm empty or from file',
tags=['RSForm']
)
@api_view(['POST']) @api_view(['POST'])
def create_rsform(request): def create_rsform(request):
''' Endpoint: Create RSForm from user input and/or trs file. ''' ''' Endpoint: Create RSForm from user input and/or trs file. '''
@ -334,6 +442,13 @@ def _prepare_rsform_data(data: dict, request, owner: m.User):
is_canonical = request.data['is_canonical'] == 'true' is_canonical = request.data['is_canonical'] == 'true'
data['is_canonical'] = is_canonical data['is_canonical'] = is_canonical
# TODO: define schema for response
@extend_schema(
request=s.ExpressionSerializer,
summary='RS expression into Syntax Tree',
tags=['Functions']
)
@api_view(['POST']) @api_view(['POST'])
def parse_expression(request): def parse_expression(request):
''' Endpoint: Parse RS expression. ''' ''' Endpoint: Parse RS expression. '''
@ -344,6 +459,12 @@ def parse_expression(request):
return Response(json.loads(result)) return Response(json.loads(result))
@extend_schema(
request=s.ExpressionSerializer,
responses={200: s.ResultTextSerializer},
summary='Unicode syntax to ASCII TeX',
tags=['Functions']
)
@api_view(['POST']) @api_view(['POST'])
def convert_to_ascii(request): def convert_to_ascii(request):
''' Endpoint: Convert expression to ASCII syntax. ''' ''' Endpoint: Convert expression to ASCII syntax. '''
@ -354,6 +475,12 @@ def convert_to_ascii(request):
return Response({'result': result}) return Response({'result': result})
@extend_schema(
request=s.ExpressionSerializer,
responses={200: s.ResultTextSerializer},
summary='ASCII TeX syntax to Unicode symbols',
tags=['Functions']
)
@api_view(['POST']) @api_view(['POST'])
def convert_to_math(request): def convert_to_math(request):
''' Endpoint: Convert expression to MATH syntax. ''' ''' Endpoint: Convert expression to MATH syntax. '''

View File

@ -9,6 +9,7 @@ from . import models
class LoginSerializer(serializers.Serializer): class LoginSerializer(serializers.Serializer):
''' Serializer: User authentification by login/password. ''' ''' Serializer: User authentification by login/password. '''
# TODO: declare schema
username = serializers.CharField( username = serializers.CharField(
label='Имя пользователя', label='Имя пользователя',
write_only=True write_only=True
@ -43,6 +44,7 @@ class LoginSerializer(serializers.Serializer):
class AuthSerializer(serializers.Serializer): class AuthSerializer(serializers.Serializer):
''' Serializer: Authentication data. ''' ''' Serializer: Authentication data. '''
# TODO: declare schema
def to_representation(self, instance: models.User) -> dict: def to_representation(self, instance: models.User) -> dict:
if instance.is_anonymous: if instance.is_anonymous:
return { return {

View File

@ -3,10 +3,14 @@ from django.contrib.auth import login, logout
from rest_framework import status, permissions, views, generics from rest_framework import status, permissions, views, generics
from rest_framework.response import Response from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, extend_schema_view
from . import serializers from . import serializers
from . import models from . import models
@extend_schema(tags=['Auth'])
@extend_schema_view()
class LoginAPIView(views.APIView): class LoginAPIView(views.APIView):
''' Endpoint: Login via username + password. ''' ''' Endpoint: Login via username + password. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -22,6 +26,8 @@ class LoginAPIView(views.APIView):
return Response(None, status=status.HTTP_202_ACCEPTED) return Response(None, status=status.HTTP_202_ACCEPTED)
@extend_schema(tags=['Auth'])
@extend_schema_view()
class LogoutAPIView(views.APIView): class LogoutAPIView(views.APIView):
''' Endpoint: Logout. ''' ''' Endpoint: Logout. '''
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
@ -31,12 +37,16 @@ class LogoutAPIView(views.APIView):
return Response(None, status=status.HTTP_204_NO_CONTENT) return Response(None, status=status.HTTP_204_NO_CONTENT)
@extend_schema(tags=['User'])
@extend_schema_view()
class SignupAPIView(generics.CreateAPIView): class SignupAPIView(generics.CreateAPIView):
''' Endpoint: Register user. ''' ''' Endpoint: Register user. '''
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
serializer_class = serializers.SignupSerializer serializer_class = serializers.SignupSerializer
@extend_schema(tags=['Auth'])
@extend_schema_view()
class AuthAPIView(generics.RetrieveAPIView): class AuthAPIView(generics.RetrieveAPIView):
''' Endpoint: Current user info. ''' ''' Endpoint: Current user info. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -46,6 +56,8 @@ class AuthAPIView(generics.RetrieveAPIView):
return self.request.user return self.request.user
@extend_schema(tags=['User'])
@extend_schema_view()
class ActiveUsersView(generics.ListAPIView): class ActiveUsersView(generics.ListAPIView):
''' Endpoint: Get list of active users. ''' ''' Endpoint: Get list of active users. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -55,6 +67,8 @@ class ActiveUsersView(generics.ListAPIView):
return models.User.objects.filter(is_active=True) return models.User.objects.filter(is_active=True)
@extend_schema(tags=['User'])
@extend_schema_view()
class UserProfileAPIView(generics.RetrieveUpdateAPIView): class UserProfileAPIView(generics.RetrieveUpdateAPIView):
''' Endpoint: User profile. ''' ''' Endpoint: User profile. '''
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
@ -64,6 +78,8 @@ class UserProfileAPIView(generics.RetrieveUpdateAPIView):
return self.request.user return self.request.user
@extend_schema(tags=['Auth'])
@extend_schema_view()
class UpdatePassword(views.APIView): class UpdatePassword(views.APIView):
''' Endpoint: Change password for current user. ''' ''' Endpoint: Change password for current user. '''
permission_classes = (permissions.IsAuthenticated, ) permission_classes = (permissions.IsAuthenticated, )

View File

@ -4,7 +4,7 @@
warn_return_any = True warn_return_any = True
warn_unused_configs = True warn_unused_configs = True
plugins = mypy_drf_plugin.main, mypy_django_plugin.main plugins = mypy_django_plugin.main
# Per-module options: # Per-module options:
[mypy.plugins.django-stubs] [mypy.plugins.django-stubs]

View File

@ -1,4 +1,4 @@
""" '''
Django settings for project. Django settings for project.
Generated by 'django-admin startproject' using Django 4.1.7. Generated by 'django-admin startproject' using Django 4.1.7.
@ -8,7 +8,7 @@ https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/ https://docs.djangoproject.com/en/4.1/ref/settings/
""" '''
import os import os
from pathlib import Path from pathlib import Path
@ -27,10 +27,7 @@ SECRET_KEY = os.environ.get('SECRET_KEY', 'not-a-secret')
DEBUG = os.environ.get('DEBUG', True) in [True, 'True', '1'] DEBUG = os.environ.get('DEBUG', True) in [True, 'True', '1']
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(';') ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(';')
INTERNAL_IPS = ['127.0.0.1'] if DEBUG else []
INTERNAL_IPS = [
"127.0.0.1",
]
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -47,12 +44,14 @@ INSTALLED_APPS = [
'apps.users', 'apps.users',
'apps.rsform', 'apps.rsform',
'drf_spectacular',
] ]
REST_FRAMEWORK = { REST_FRAMEWORK = {
'TEST_REQUEST_DEFAULT_FORMAT': 'json', 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
], ],
@ -64,7 +63,7 @@ REST_FRAMEWORK = {
], ],
} }
# CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000').split(';') CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000').split(';')
CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000').split(';') CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000').split(';')
@ -74,8 +73,6 @@ if _domain != '':
CSRF_COOKIE_DOMAIN = _domain CSRF_COOKIE_DOMAIN = _domain
SESSION_COOKIE_DOMAIN = _domain SESSION_COOKIE_DOMAIN = _domain
# CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@ -93,6 +90,16 @@ LOGIN_URL = '/admin/login/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
STATIC_URL = 'static/'
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
MEDIA_URL = 'media/'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -126,6 +133,15 @@ DATABASES = {
} }
} }
# drf-spectacular settings. API docs generator
# https://drf-spectacular.readthedocs.io/en/latest/settings.html
SPECTACULAR_SETTINGS = {
'TITLE': 'ConceptPortal API',
'DESCRIPTION': 'Портал для работы с экспликациями концептуальных схем',
'VERSION': '0.1.0',
'SERVE_INCLUDE_SCHEMA': False,
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
@ -156,14 +172,6 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
STATIC_URL = 'static/'
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
MEDIA_URL = 'media/'
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

View File

@ -1,15 +1,18 @@
''' Main URL router ''' ''' Main URL router '''
from rest_framework.documentation import include_docs_urls
from django.contrib import admin from django.contrib import admin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import path, include from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin', admin.site.urls),
path('api/', include('apps.rsform.urls')), path('api/', include('apps.rsform.urls')),
path('users/', include('apps.users.urls')), path('users/', include('apps.users.urls')),
path('docs/', include_docs_urls(title='ConceptPortal API'), name='docs'), path('docs', SpectacularSwaggerView.as_view(), name='docs'),
path('', lambda request: redirect('docs/', permanent=True)), path('schema', SpectacularAPIView.as_view(), name='schema'),
path('redoc', SpectacularRedocView.as_view()),
path('', lambda: redirect('docs', permanent=True)),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -3,6 +3,7 @@ django
djangorestframework djangorestframework
django-cors-headers django-cors-headers
django-filter django-filter
drf-spectacular
coreapi coreapi
pymorphy2 pymorphy2
pymorphy2-dicts-ru pymorphy2-dicts-ru

View File

@ -3,6 +3,7 @@ django
djangorestframework djangorestframework
django-cors-headers django-cors-headers
django-filter django-filter
drf-spectacular
coreapi coreapi
pymorphy2 pymorphy2
pymorphy2-dicts-ru pymorphy2-dicts-ru
@ -12,5 +13,4 @@ razdel
mypy mypy
pylint pylint
coverage coverage
django-stubs[compatible-mypy]
djangorestframework-stubs[compatible-mypy] djangorestframework-stubs[compatible-mypy]

View File

@ -1,80 +1,52 @@
// Module: Natural language model declarations. // Module: Natural language model declarations.
/**
// ====== Morphology ======== * Represents single unit of language Morphology.
*/
export enum Grammeme { export enum Grammeme {
// Неизвестная граммема // Неизвестная граммема
UNKN = 'UNKN', UNKN = 'UNKN',
// Части речи // Части речи
NOUN = 'NOUN', NOUN = 'NOUN', ADJF = 'ADJF', ADJS = 'ADJS', COMP = 'COMP',
ADJF = 'ADJF', VERB = 'VERB', INFN = 'INFN', PRTF = 'PRTF', PRTS = 'PRTS',
ADJS = 'ADJS', GRND = 'GRND', NUMR = 'NUMR', ADVB = 'ADVB', NPRO = 'NPRO',
COMP = 'COMP', PRED = 'PRED', PREP = 'PREP', CONJ = 'CONJ', PRCL = 'PRCL',
VERB = 'VERB',
INFN = 'INFN',
PRTF = 'PRTF',
PRTS = 'PRTS',
GRND = 'GRND',
NUMR = 'NUMR',
ADVB = 'ADVB',
NPRO = 'NPRO',
PRED = 'PRED',
PREP = 'PREP',
CONJ = 'CONJ',
PRCL = 'PRCL',
INTJ = 'INTJ', INTJ = 'INTJ',
// Одушевленность // Одушевленность
anim = 'anim', anim = 'anim', inan = 'inan',
inan = 'inan',
// Род // Род
masc = 'masc', masc = 'masc', femn = 'femn', neut = 'neut',
femn = 'femn',
neut = 'neut',
// Число // Число
sing = 'sing', sing = 'sing', plur = 'plur',
plur = 'plur',
// Падеж (основные) // Падеж (основные)
nomn = 'nomn', nomn = 'nomn', gent = 'gent', datv = 'datv',
gent = 'gent', accs = 'accs', ablt = 'ablt', loct = 'loct',
datv = 'datv',
accs = 'accs',
ablt = 'ablt',
loct = 'loct',
// Совершенный / несовершенный вид // Совершенный / несовершенный вид
perf = 'perf', perf = 'perf', impf = 'impf',
impf = 'impf',
// Переходность // Переходность
tran = 'tran', tran = 'tran', intr = 'intr',
intr = 'intr',
// Время // Время
pres = 'pres', pres = 'pres', past = 'past', futr = 'futr',
past = 'past',
futr = 'futr',
// Лицо // Лицо
per1 = '1per', per1 = '1per', per2 = '2per', per3 = '3per',
per2 = '2per',
per3 = '3per',
// Наклонение // Наклонение
indc = 'indc', indc = 'indc', impr = 'impr',
impr = 'impr',
// Включение говорящего в действие // Включение говорящего в действие
incl = 'incl', incl = 'incl', excl = 'excl',
excl = 'excl',
// Залог // Залог
actv = 'actv', actv = 'actv', pssv = 'pssv',
pssv = 'pssv',
// Стиль речи // Стиль речи
Infr = 'Infr', // Неформальный Infr = 'Infr', // Неформальный
@ -86,6 +58,11 @@ export enum Grammeme {
Abbr = 'Abbr' Abbr = 'Abbr'
} }
/**
* Represents part of speech language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const PartOfSpeech = [ export const PartOfSpeech = [
Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS, Grammeme.COMP, Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS, Grammeme.COMP,
Grammeme.VERB, Grammeme.INFN, Grammeme.PRTF, Grammeme.PRTS, Grammeme.VERB, Grammeme.INFN, Grammeme.PRTF, Grammeme.PRTS,
@ -93,40 +70,106 @@ export const PartOfSpeech = [
Grammeme.PREP, Grammeme.CONJ, Grammeme.PRCL, Grammeme.INTJ Grammeme.PREP, Grammeme.CONJ, Grammeme.PRCL, Grammeme.INTJ
]; ];
/**
* Represents gender language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Gender = [ export const Gender = [
Grammeme.masc, Grammeme.femn, Grammeme.neut Grammeme.masc, Grammeme.femn, Grammeme.neut
]; ];
/**
* Represents case language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Case = [ export const Case = [
Grammeme.nomn, Grammeme.gent, Grammeme.datv, Grammeme.nomn, Grammeme.gent, Grammeme.datv,
Grammeme.accs, Grammeme.ablt, Grammeme.loct Grammeme.accs, Grammeme.ablt, Grammeme.loct
]; ];
/**
* Represents plurality language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Plurality = [ Grammeme.sing, Grammeme.plur ]; export const Plurality = [ Grammeme.sing, Grammeme.plur ];
/**
* Represents verb perfectivity language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Perfectivity = [ Grammeme.perf, Grammeme.impf ]; export const Perfectivity = [ Grammeme.perf, Grammeme.impf ];
/**
* Represents verb transitivity language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Transitivity = [ Grammeme.tran, Grammeme.intr ]; export const Transitivity = [ Grammeme.tran, Grammeme.intr ];
/**
* Represents verb mood language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Mood = [ Grammeme.indc, Grammeme.impr ]; export const Mood = [ Grammeme.indc, Grammeme.impr ];
/**
* Represents verb self-inclusion language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Inclusion = [ Grammeme.incl, Grammeme.excl ]; export const Inclusion = [ Grammeme.incl, Grammeme.excl ];
/**
* Represents verb voice language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Voice = [ Grammeme.actv, Grammeme.pssv ]; export const Voice = [ Grammeme.actv, Grammeme.pssv ];
/**
* Represents verb tense language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Tense = [ export const Tense = [
Grammeme.pres, Grammeme.pres,
Grammeme.past, Grammeme.past,
Grammeme.futr Grammeme.futr
]; ];
/**
* Represents verb person language concept.
*
* Implemented as a list of mututally exclusive {@link Grammeme}s.
*/
export const Person = [ export const Person = [
Grammeme.per1, Grammeme.per1,
Grammeme.per2, Grammeme.per2,
Grammeme.per3 Grammeme.per3
]; ];
/**
* Represents complete list of language concepts.
*
* Implemented as a list of lists of {@link Grammeme}s.
*/
export const GrammemeGroups = [ export const GrammemeGroups = [
PartOfSpeech, Gender, Case, Plurality, Perfectivity, PartOfSpeech, Gender, Case, Plurality, Perfectivity,
Transitivity, Mood, Inclusion, Voice, Tense, Person Transitivity, Mood, Inclusion, Voice, Tense, Person
]; ];
/**
* Represents NOUN-ish list of language concepts.
*
* Represented concepts can be target of inflection or coalition in a sentence.
*
* Implemented as a list of lists of {@link Grammeme}s.
*/
export const NounGrams = [ export const NounGrams = [
Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS, Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS,
...Gender, ...Gender,
@ -134,6 +177,13 @@ export const NounGrams = [
...Plurality ...Plurality
]; ];
/**
* Represents VERB-ish list of language concepts.
*
* Represented concepts can be target of inflection or coalition in a sentence.
*
* Implemented as a list of lists of {@link Grammeme}s.
*/
export const VerbGrams = [ export const VerbGrams = [
Grammeme.VERB, Grammeme.INFN, Grammeme.PRTF, Grammeme.PRTS, Grammeme.VERB, Grammeme.INFN, Grammeme.PRTF, Grammeme.PRTS,
...Perfectivity, ...Perfectivity,
@ -145,18 +195,45 @@ export const VerbGrams = [
...Person ...Person
]; ];
// Grammeme parse data /**
* Represents {@link Grammeme} parse data.
*/
export interface IGramData { export interface IGramData {
type: Grammeme type: Grammeme
data: string data: string
} }
// Equality comparator for IGramData /**
export function matchGrammeme(value: IGramData, test: IGramData): boolean { * Represents specific wordform attached to {@link Grammeme}s.
if (value.type !== test.type) { */
export interface IWordForm {
text: string
grams: IGramData[]
}
/**
* Equality comparator for {@link IGramData}. Compares text data for unknown grammemes
*/
export function matchGrammeme(left: IGramData, right: IGramData): boolean {
if (left.type !== right.type) {
return false; return false;
} }
return value.type !== Grammeme.UNKN || value.data === test.data; return left.type !== Grammeme.UNKN || left.data === right.data;
}
/**
* Equality comparator for {@link IWordForm}. Compares a set of Grammemes attached to wordforms
*/
export function matchWordForm(left: IWordForm, right: IWordForm): boolean {
if (left.grams.length !== right.grams.length) {
return false;
}
for (let index = 0; index < left.grams.length; ++index) {
if (!matchGrammeme(left.grams[index], right.grams[index])) {
return false;
}
}
return true;
} }
function parseSingleGrammeme(text: string): IGramData { function parseSingleGrammeme(text: string): IGramData {
@ -173,6 +250,18 @@ function parseSingleGrammeme(text: string): IGramData {
} }
} }
export function sortGrammemes<TData extends IGramData>(input: TData[]): TData[] {
const result: TData[] = [];
Object.values(Grammeme).forEach(
gram => {
const item = input.find(data => data.type === gram);
if (item) {
result.push(item);
}
});
return result;
}
export function parseGrammemes(termForm: string): IGramData[] { export function parseGrammemes(termForm: string): IGramData[] {
const result: IGramData[] = []; const result: IGramData[] = [];
const chunks = termForm.split(','); const chunks = termForm.split(',');
@ -182,12 +271,7 @@ export function parseGrammemes(termForm: string): IGramData[] {
result.push(parseSingleGrammeme(chunk)); result.push(parseSingleGrammeme(chunk));
} }
}); });
return result; return sortGrammemes(result);
}
export interface IWordForm {
text: string
grams: IGramData[]
} }
// ====== Reference resolution ===== // ====== Reference resolution =====

View File

@ -7,8 +7,14 @@ import SelectMulti from '../../components/Common/SelectMulti';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import DataTable, { createColumnHelper } from '../../components/DataTable'; import DataTable, { createColumnHelper } from '../../components/DataTable';
import { CheckIcon, ChevronDoubleUpIcon, ChevronUpIcon, CrossIcon } from '../../components/Icons'; import { CheckIcon, ChevronDoubleUpIcon, ChevronUpIcon, CrossIcon } from '../../components/Icons';
import { Grammeme, GrammemeGroups, IWordForm, NounGrams, parseGrammemes,VerbGrams } from '../../models/language'; import { useConceptTheme } from '../../context/ThemeContext';
import {
Grammeme, GrammemeGroups, IWordForm,
matchWordForm, NounGrams, parseGrammemes,
sortGrammemes, VerbGrams
} from '../../models/language';
import { IConstituenta, TermForm } from '../../models/rsform'; import { IConstituenta, TermForm } from '../../models/rsform';
import { colorfgGrammeme } from '../../utils/color';
import { labelGrammeme } from '../../utils/labels'; import { labelGrammeme } from '../../utils/labels';
import { IGrammemeOption, SelectorGrammems } from '../../utils/selectors'; import { IGrammemeOption, SelectorGrammems } from '../../utils/selectors';
@ -21,6 +27,7 @@ interface DlgEditTermProps {
const columnHelper = createColumnHelper<IWordForm>(); const columnHelper = createColumnHelper<IWordForm>();
function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
const { colors } = useConceptTheme();
const [term, setTerm] = useState(''); const [term, setTerm] = useState('');
const [inputText, setInputText] = useState(''); const [inputText, setInputText] = useState('');
@ -50,55 +57,73 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
})); }));
setForms(initForms); setForms(initForms);
setTerm(target.term_resolved); setTerm(target.term_resolved);
setInputText(target.term_resolved);
}, [target]); }, [target]);
// Filter grammemes when input changes // Filter grammemes when input changes
useEffect( useEffect(
() => { () => {
let newFilter: Grammeme[] = []; let newFilter: Grammeme[] = [];
inputGrams.forEach(({value: gram}) => { inputGrams.forEach(({type: gram}) => {
if (!newFilter.includes(gram)) { if (!newFilter.includes(gram)) {
if (NounGrams.includes(gram)) { if (NounGrams.includes(gram)) {
newFilter.push(...NounGrams); newFilter.push(...NounGrams);
} }
if (VerbGrams.includes(gram)) { if (VerbGrams.includes(gram)) {
newFilter.push(...NounGrams); newFilter.push(...VerbGrams);
} }
} }
}); });
inputGrams.forEach(({value: gram}) => inputGrams.forEach(({type: gram}) =>
GrammemeGroups.forEach(group => { GrammemeGroups.forEach(group => {
if (group.includes(gram)) { if (group.includes(gram)) {
newFilter = newFilter.filter(item => !group.includes(item) || item === gram); newFilter = newFilter.filter(item => !group.includes(item) || item === gram);
} }
})); }));
newFilter.push(...inputGrams.map(({value: gram}) => gram)); newFilter.push(...inputGrams.map(({type: gram}) => gram));
if (newFilter.length === 0) { if (newFilter.length === 0) {
newFilter = [...VerbGrams, ...NounGrams]; newFilter = [...VerbGrams, ...NounGrams];
} }
newFilter = [... new Set(newFilter)]; newFilter = [... new Set(newFilter)];
setOptions(SelectorGrammems.filter(({value: gram}) => newFilter.includes(gram))); setOptions(SelectorGrammems.filter(({type: gram}) => newFilter.includes(gram)));
}, [inputGrams]); }, [inputGrams]);
const handleSubmit = () => onSave(getData()); const handleSubmit = () => onSave(getData());
function handleAddForm() { function handleAddForm() {
const newForm: IWordForm = {
text: inputText,
grams: inputGrams.map(item => ({
type: item.type,
data: item.data
}))
};
setForms(forms => [ setForms(forms => [
...forms, ...forms.filter(value => !matchWordForm(value, newForm)),
{ newForm
text: inputText,
grams: inputGrams.map(item => ({
type: item.value, data: item.value as string
}))
}
]); ]);
} }
function handleResetForm() { function handleDeleteRow(row: number) {
setForms(
(prev) => {
const newForms: IWordForm[] = [];
prev.forEach(
(form, index) => {
if (index !== row) {
newForms.push(form);
}
});
return newForms;
});
}
function handleResetForm() {
setInputText('');
setInputGrams([]);
} }
function handleGenerateSelected() { function handleGenerateSelected() {
@ -106,6 +131,11 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
} }
function handleGenerateBasics() { function handleGenerateBasics() {
if (forms.length > 0) {
if (!window.confirm('Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?')) {
return;
}
}
} }
@ -116,7 +146,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
header: 'Текст', header: 'Текст',
size: 350, size: 350,
minSize: 350, minSize: 350,
maxSize: 350 maxSize: 350,
cell: props => <div className='min-w-[20rem]'>{props.getValue()}</div>
}), }),
columnHelper.accessor('grams', { columnHelper.accessor('grams', {
id: 'grams', id: 'grams',
@ -124,41 +155,42 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
size: 250, size: 250,
minSize: 250, minSize: 250,
maxSize: 250, maxSize: 250,
cell: props => { cell: props =>
return (
<div className='flex justify-start gap-1 select-none'> <div className='flex justify-start gap-1 select-none'>
{ props.getValue().map( { props.getValue().map(
data => (<> gram =>
<div <div
className='min-w-[3rem] px-1 text-center rounded-md whitespace-nowrap border clr-border clr-input' className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'
title='' title=''
// style={{ style={{
// borderWidth: '1px', borderWidth: '1px',
// borderColor: getCstStatusFgColor(cst.status, colors), borderColor: colorfgGrammeme(gram.type, colors),
// color: getCstStatusFgColor(cst.status, colors), color: colorfgGrammeme(gram.type, colors),
// fontWeight: 600, fontWeight: 600,
// backgroundColor: isMockCst(cst) ? colors.bgWarning : colors.bgInput backgroundColor: colors.bgInput
// }} }}
> >
{labelGrammeme(data)} {labelGrammeme(gram)}
</div> </div>
{/* <ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} /> */} )}
</>))} </div>
</div>);
}
// cell: props =>
// <div style={{
// fontSize: 12,
// color: isMockCst(props.row.original) ? colors.fgWarning : undefined
// }}>
// {props.getValue()}
// </div>
}), }),
// columnHelper.accessor(, { columnHelper.display({
id: 'actions',
// }) size: 50,
], []); minSize: 50,
maxSize: 50,
cell: props =>
<div>
<MiniButton
tooltip='Удалить словоформу'
icon={<CrossIcon size={4} color='text-warning'/>}
noHover
onClick={() => handleDeleteRow(props.row.index)}
/>
</div>
})
], [colors]);
return ( return (
<Modal <Modal
@ -193,7 +225,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
<div className='flex items-center justify-start'> <div className='flex items-center justify-start'>
<MiniButton <MiniButton
tooltip='Добавить словоформу' tooltip='Добавить словоформу'
icon={<CheckIcon size={6} color='text-success'/>} icon={<CheckIcon size={6} color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}/>}
disabled={!inputText || inputGrams.length == 0}
onClick={handleAddForm} onClick={handleAddForm}
/> />
<MiniButton <MiniButton
@ -203,7 +236,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
/> />
<MiniButton <MiniButton
tooltip='Генерировать словоформу' tooltip='Генерировать словоформу'
icon={<ChevronUpIcon size={6} color='text-primary'/>} icon={<ChevronUpIcon size={6} color={inputGrams.length == 0 ? 'text-disabled' : 'text-primary'}/>}
disabled={inputGrams.length == 0}
onClick={handleGenerateSelected} onClick={handleGenerateSelected}
/> />
<MiniButton <MiniButton
@ -219,11 +253,11 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
placeholder='Выберите граммемы' placeholder='Выберите граммемы'
value={inputGrams} value={inputGrams}
onChange={data => setInputGrams(data.map(value => value))} onChange={newValue => setInputGrams(sortGrammemes([...newValue]))}
/> />
</div> </div>
<div className='border overflow-y-auto max-h-[20rem]'> <div className='border overflow-y-auto max-h-[17.4rem] min-h-[17.4rem]'>
<DataTable <DataTable
data={forms} data={forms}
columns={columns} columns={columns}
@ -232,6 +266,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
noDataComponent={ noDataComponent={
<span className='flex flex-col justify-center p-2 text-center min-h-[2rem]'> <span className='flex flex-col justify-center p-2 text-center min-h-[2rem]'>
<p>Список пуст</p> <p>Список пуст</p>
<p>Добавьте словоформу</p>
</span> </span>
} }

View File

@ -1,5 +1,6 @@
// =========== Modules contains all dynamic color definitions ========== // =========== Modules contains all dynamic color definitions ==========
import { Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '../models/language'
import { CstClass, ExpressionStatus } from '../models/rsform' import { CstClass, ExpressionStatus } from '../models/rsform'
import { ISyntaxTreeNode, TokenID } from '../models/rslang' import { ISyntaxTreeNode, TokenID } from '../models/rslang'
@ -384,3 +385,29 @@ export function colorbgCstClass(cstClass: CstClass, colors: IColorTheme): string
} }
} }
export function colorfgGrammeme(gram: Grammeme, colors: IColorTheme): string {
if (PartOfSpeech.includes(gram)) {
return colors.fgBlue;
}
if (NounGrams.includes(gram)) {
return colors.fgGreen;
}
if (VerbGrams.includes(gram)) {
return colors.fgTeal;
}
return colors.fgDefault;
}
export function colorbgGrammeme(gram: Grammeme, colors: IColorTheme): string {
if (PartOfSpeech.includes(gram)) {
return colors.bgBlue;
}
if (NounGrams.includes(gram)) {
return colors.bgGreen;
}
if (VerbGrams.includes(gram)) {
return colors.bgTeal;
}
return colors.bgInput;
}

View File

@ -21,7 +21,7 @@ export const urls = {
gitrepo: 'https://github.com/IRBorisov/ConceptPortal', gitrepo: 'https://github.com/IRBorisov/ConceptPortal',
mailportal: 'mailto:portal@acconcept.ru', mailportal: 'mailto:portal@acconcept.ru',
restapi: 'https://api.portal.acconcept.ru/docs/' restapi: 'https://api.portal.acconcept.ru/docs'
}; };
export const resources = { export const resources = {

View File

@ -38,7 +38,7 @@ export const SelectorCstType = (
}) })
); );
export interface IGrammemeOption { export interface IGrammemeOption extends IGramData {
value: Grammeme value: Grammeme
label: string label: string
} }
@ -63,6 +63,8 @@ export const SelectorGrammems: IGrammemeOption[] =
Grammeme.pssv, Grammeme.actv, Grammeme.pssv, Grammeme.actv,
].map( ].map(
gram => ({ gram => ({
type: gram,
data: gram as string,
value: gram, value: gram,
label: labelGrammeme({type: gram, data: ''} as IGramData) label: labelGrammeme({type: gram, data: ''} as IGramData)
})); }));