mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactor pyconcept interaction and minor UI fixes
This commit is contained in:
parent
05645a29e8
commit
cff07788e5
|
@ -430,7 +430,8 @@ disable=too-many-public-methods,
|
||||||
unused-argument,
|
unused-argument,
|
||||||
missing-function-docstring,
|
missing-function-docstring,
|
||||||
attribute-defined-outside-init,
|
attribute-defined-outside-init,
|
||||||
ungrouped-imports
|
ungrouped-imports,
|
||||||
|
abstract-method
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.db import migrations
|
||||||
|
|
||||||
from apps.rsform import utils
|
from apps.rsform import utils
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import RSForm
|
||||||
|
from apps.rsform.serializers import RSFormTRSSerializer
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +12,10 @@ def load_initial_schemas(apps, schema_editor):
|
||||||
for subdir, dirs, files in os.walk(rootdir):
|
for subdir, dirs, files in os.walk(rootdir):
|
||||||
for file in files:
|
for file in files:
|
||||||
data = utils.read_trs(os.path.join(subdir, file))
|
data = utils.read_trs(os.path.join(subdir, file))
|
||||||
RSForm.create_from_trs(None, data)
|
data['is_common'] = True
|
||||||
|
serializer = RSFormTRSSerializer(data=data, context={'load_meta': True})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
def load_initial_users(apps, schema_editor):
|
def load_initial_users(apps, schema_editor):
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
''' Models: RSForms for conceptual schemas. '''
|
''' Models: RSForms for conceptual schemas. '''
|
||||||
import json
|
import json
|
||||||
from typing import Iterable, Optional
|
from copy import deepcopy
|
||||||
import pyconcept
|
import re
|
||||||
|
from typing import Iterable, Optional, cast
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet,
|
CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet,
|
||||||
|
@ -10,9 +12,16 @@ from django.db.models import (
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
import pyconcept
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from cctext import Resolver, Entity, extract_entities
|
from cctext import Resolver, Entity, extract_entities
|
||||||
from .graph import Graph
|
from .graph import Graph
|
||||||
|
from .utils import apply_mapping_pattern
|
||||||
|
|
||||||
|
|
||||||
|
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
|
||||||
|
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)')
|
||||||
|
|
||||||
|
|
||||||
class CstType(TextChoices):
|
class CstType(TextChoices):
|
||||||
|
@ -37,6 +46,26 @@ class Syntax(TextChoices):
|
||||||
def _empty_forms():
|
def _empty_forms():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _get_type_prefix(cst_type: CstType) -> str:
|
||||||
|
''' Get alias prefix. '''
|
||||||
|
if cst_type == CstType.BASE:
|
||||||
|
return 'X'
|
||||||
|
if cst_type == CstType.CONSTANT:
|
||||||
|
return 'C'
|
||||||
|
if cst_type == CstType.STRUCTURED:
|
||||||
|
return 'S'
|
||||||
|
if cst_type == CstType.AXIOM:
|
||||||
|
return 'A'
|
||||||
|
if cst_type == CstType.TERM:
|
||||||
|
return 'D'
|
||||||
|
if cst_type == CstType.FUNCTION:
|
||||||
|
return 'F'
|
||||||
|
if cst_type == CstType.PREDICATE:
|
||||||
|
return 'P'
|
||||||
|
if cst_type == CstType.THEOREM:
|
||||||
|
return 'T'
|
||||||
|
return 'X'
|
||||||
|
|
||||||
|
|
||||||
class RSForm(Model):
|
class RSForm(Model):
|
||||||
''' RSForm is a math form of capturing conceptual schema '''
|
''' RSForm is a math form of capturing conceptual schema '''
|
||||||
|
@ -134,7 +163,7 @@ class RSForm(Model):
|
||||||
alias=alias,
|
alias=alias,
|
||||||
cst_type=insert_type
|
cst_type=insert_type
|
||||||
)
|
)
|
||||||
self._update_order()
|
self.update_order()
|
||||||
self.save()
|
self.save()
|
||||||
result.refresh_from_db()
|
result.refresh_from_db()
|
||||||
return result
|
return result
|
||||||
|
@ -151,7 +180,7 @@ class RSForm(Model):
|
||||||
alias=alias,
|
alias=alias,
|
||||||
cst_type=insert_type
|
cst_type=insert_type
|
||||||
)
|
)
|
||||||
self._update_order()
|
self.update_order()
|
||||||
self.save()
|
self.save()
|
||||||
result.refresh_from_db()
|
result.refresh_from_db()
|
||||||
return result
|
return result
|
||||||
|
@ -177,7 +206,7 @@ class RSForm(Model):
|
||||||
count_moved += 1
|
count_moved += 1
|
||||||
update_list.append(cst)
|
update_list.append(cst)
|
||||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||||
self._update_order()
|
self.update_order()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -185,8 +214,8 @@ class RSForm(Model):
|
||||||
''' Delete multiple constituents. Do not check if listCst are from this schema '''
|
''' Delete multiple constituents. Do not check if listCst are from this schema '''
|
||||||
for cst in listCst:
|
for cst in listCst:
|
||||||
cst.delete()
|
cst.delete()
|
||||||
self._update_order()
|
self.update_order()
|
||||||
self._resolve_all_text()
|
self.resolve_all_text()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -209,89 +238,61 @@ class RSForm(Model):
|
||||||
cst.refresh_from_db()
|
cst.refresh_from_db()
|
||||||
return cst
|
return cst
|
||||||
|
|
||||||
def _insert_new(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta':
|
def reset_aliases(self):
|
||||||
if insert_after is not None:
|
''' Recreate all aliases based on cst order. '''
|
||||||
cstafter = Constituenta.objects.get(pk=insert_after)
|
mapping = self._create_reset_mapping()
|
||||||
return self.insert_at(cstafter.order + 1, data['alias'], data['cst_type'])
|
self._apply_mapping(mapping)
|
||||||
else:
|
|
||||||
return self.insert_last(data['alias'], data['cst_type'])
|
def _create_reset_mapping(self) -> dict[str, str]:
|
||||||
|
bases = cast(dict[str, int], {})
|
||||||
|
mapping = cast(dict[str, str], {})
|
||||||
|
for cst_type in CstType.values:
|
||||||
|
bases[cst_type] = 1
|
||||||
|
cst_list = self.constituents().order_by('order')
|
||||||
|
for cst in cst_list:
|
||||||
|
alias = f'{_get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
|
||||||
|
bases[cst.cst_type] += 1
|
||||||
|
if cst.alias != alias:
|
||||||
|
mapping[cst.alias] = alias
|
||||||
|
return mapping
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def load_trs(self, data: dict, sync_metadata: bool, skip_update: bool):
|
def _apply_mapping(self, mapping: dict[str, str]):
|
||||||
if sync_metadata:
|
cst_list = self.constituents().order_by('order')
|
||||||
self.title = data.get('title', 'Без названия')
|
for cst in cst_list:
|
||||||
self.alias = data.get('alias', '')
|
modified = False
|
||||||
self.comment = data.get('comment', '')
|
if cst.alias in mapping:
|
||||||
order = 1
|
modified = True
|
||||||
prev_constituents = self.constituents()
|
cst.alias = mapping[cst.alias]
|
||||||
loaded_ids = set()
|
expression = apply_mapping_pattern(cst.definition_formal, mapping, _GLOBAL_ID_PATTERN)
|
||||||
for cst_data in data['items']:
|
if expression != cst.definition_formal:
|
||||||
uid = int(cst_data['entityUID'])
|
modified = True
|
||||||
if prev_constituents.filter(pk=uid).exists():
|
cst.definition_formal = expression
|
||||||
cst: Constituenta = prev_constituents.get(pk=uid)
|
convention = apply_mapping_pattern(cst.convention, mapping, _GLOBAL_ID_PATTERN)
|
||||||
cst.order = order
|
if convention != cst.convention:
|
||||||
cst.load_trs(cst_data)
|
modified = True
|
||||||
|
cst.convention = convention
|
||||||
|
term = apply_mapping_pattern(cst.term_raw, mapping, _REF_ENTITY_PATTERN)
|
||||||
|
if term != cst.term_raw:
|
||||||
|
modified = True
|
||||||
|
cst.term_raw = term
|
||||||
|
definition = apply_mapping_pattern(cst.definition_raw, mapping, _REF_ENTITY_PATTERN)
|
||||||
|
if definition != cst.definition_raw:
|
||||||
|
modified = True
|
||||||
|
cst.definition_raw = definition
|
||||||
|
if modified:
|
||||||
cst.save()
|
cst.save()
|
||||||
else:
|
|
||||||
cst = Constituenta.create_from_trs(cst_data, self, order)
|
|
||||||
cst.save()
|
|
||||||
uid = cst.pk
|
|
||||||
loaded_ids.add(uid)
|
|
||||||
order += 1
|
|
||||||
for prev_cst in prev_constituents:
|
|
||||||
if prev_cst.pk not in loaded_ids:
|
|
||||||
prev_cst.delete()
|
|
||||||
if not skip_update:
|
|
||||||
self._update_order()
|
|
||||||
self._resolve_all_text()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@transaction.atomic
|
|
||||||
def create_from_trs(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
|
||||||
schema = RSForm.objects.create(
|
|
||||||
title=data.get('title', 'Без названия'),
|
|
||||||
owner=owner,
|
|
||||||
alias=data.get('alias', ''),
|
|
||||||
comment=data.get('comment', ''),
|
|
||||||
is_common=is_common
|
|
||||||
)
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
schema._create_items_from_trs(data['items'])
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def to_trs(self) -> dict:
|
|
||||||
''' Generate JSON string containing all data from RSForm '''
|
|
||||||
result = self._prepare_json_rsform()
|
|
||||||
items = self.constituents().order_by('order')
|
|
||||||
for cst in items:
|
|
||||||
result['items'].append(cst.to_trs())
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('rsform-detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def _prepare_json_rsform(self: 'RSForm') -> dict:
|
|
||||||
return {
|
|
||||||
'type': 'rsform',
|
|
||||||
'title': self.title,
|
|
||||||
'alias': self.alias,
|
|
||||||
'comment': self.comment,
|
|
||||||
'items': []
|
|
||||||
}
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def _update_order(self):
|
def update_order(self):
|
||||||
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
|
''' Update constituents order. '''
|
||||||
|
checked = PyConceptAdapter(self).basic()
|
||||||
update_list = self.constituents().only('id', 'order')
|
update_list = self.constituents().only('id', 'order')
|
||||||
if len(checked['items']) != update_list.count():
|
if len(checked['items']) != update_list.count():
|
||||||
raise ValidationError('Invalid constituents count')
|
raise ValidationError('Invalid constituents count')
|
||||||
order = 1
|
order = 1
|
||||||
for cst in checked['items']:
|
for cst in checked['items']:
|
||||||
cst_id = cst['entityUID']
|
cst_id = cst['id']
|
||||||
for oldCst in update_list:
|
for oldCst in update_list:
|
||||||
if oldCst.pk == cst_id:
|
if oldCst.pk == cst_id:
|
||||||
oldCst.order = order
|
oldCst.order = order
|
||||||
|
@ -300,14 +301,8 @@ class RSForm(Model):
|
||||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def _create_items_from_trs(self, items):
|
def resolve_all_text(self):
|
||||||
order = 1
|
''' Trigger reference resolution for all texts. '''
|
||||||
for cst in items:
|
|
||||||
cst_object = Constituenta.create_from_trs(cst, self, order)
|
|
||||||
cst_object.save()
|
|
||||||
order += 1
|
|
||||||
|
|
||||||
def _resolve_all_text(self):
|
|
||||||
graph_terms = self._term_graph()
|
graph_terms = self._term_graph()
|
||||||
resolver = Resolver({})
|
resolver = Resolver({})
|
||||||
for alias in graph_terms.topological_order():
|
for alias in graph_terms.topological_order():
|
||||||
|
@ -323,6 +318,19 @@ class RSForm(Model):
|
||||||
cst.definition_resolved = resolved
|
cst.definition_resolved = resolved
|
||||||
cst.save()
|
cst.save()
|
||||||
|
|
||||||
|
def _insert_new(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta':
|
||||||
|
if insert_after is not None:
|
||||||
|
cstafter = Constituenta.objects.get(pk=insert_after)
|
||||||
|
return self.insert_at(cstafter.order + 1, data['alias'], data['cst_type'])
|
||||||
|
else:
|
||||||
|
return self.insert_last(data['alias'], data['cst_type'])
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.title}'
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('rsform-detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def _term_graph(self) -> Graph:
|
def _term_graph(self) -> Graph:
|
||||||
result = Graph()
|
result = Graph()
|
||||||
cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order')
|
cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order')
|
||||||
|
@ -413,8 +421,8 @@ class Constituenta(Model):
|
||||||
''' URL access. '''
|
''' URL access. '''
|
||||||
return reverse('constituenta-detail', kwargs={'pk': self.pk})
|
return reverse('constituenta-detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.alias
|
return f'{self.alias}'
|
||||||
|
|
||||||
def set_term_resolved(self, new_term: str):
|
def set_term_resolved(self, new_term: str):
|
||||||
''' Set term and reset forms if needed. '''
|
''' Set term and reset forms if needed. '''
|
||||||
|
@ -423,61 +431,83 @@ class Constituenta(Model):
|
||||||
self.term_resolved = new_term
|
self.term_resolved = new_term
|
||||||
self.term_forms = []
|
self.term_forms = []
|
||||||
|
|
||||||
@staticmethod
|
class PyConceptAdapter:
|
||||||
def create_from_trs(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
''' RSForm adapter for interacting with pyconcept module. '''
|
||||||
''' Create constituenta from TRS json '''
|
def __init__(self, instance: RSForm):
|
||||||
cst = Constituenta(
|
self.schema = instance
|
||||||
alias=data['alias'],
|
self.data = self._prepare_request()
|
||||||
schema=schema,
|
self._checked_data: Optional[dict] = None
|
||||||
order=order,
|
|
||||||
cst_type=data['cstType'],
|
|
||||||
)
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
cst._load_texts(data)
|
|
||||||
return cst
|
|
||||||
|
|
||||||
def load_trs(self, data: dict):
|
def basic(self) -> dict:
|
||||||
''' Load data from TRS json '''
|
''' Check RSForm and return check results.
|
||||||
self.alias = data['alias']
|
Warning! Does not include texts. '''
|
||||||
self.cst_type = data['cstType']
|
self._produce_response()
|
||||||
self._load_texts(data)
|
if self._checked_data is None:
|
||||||
|
raise ValueError('Invalid data response from pyconcept')
|
||||||
|
return self._checked_data
|
||||||
|
|
||||||
def _load_texts(self, data: dict):
|
def full(self) -> dict:
|
||||||
self.convention = data.get('convention', '')
|
''' Check RSForm and return check results including initial texts. '''
|
||||||
if 'definition' in data:
|
self._produce_response()
|
||||||
self.definition_formal = data['definition'].get('formal', '')
|
if self._checked_data is None:
|
||||||
if 'text' in data['definition']:
|
raise ValueError('Invalid data response from pyconcept')
|
||||||
self.definition_raw = data['definition']['text'].get('raw', '')
|
return self._complete_rsform_details(self._checked_data)
|
||||||
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) -> dict:
|
def _complete_rsform_details(self, data: dict) -> dict:
|
||||||
return {
|
result = deepcopy(data)
|
||||||
'entityUID': self.pk,
|
result['id'] = self.schema.pk
|
||||||
'type': 'constituenta',
|
result['alias'] = self.schema.alias
|
||||||
'cstType': self.cst_type,
|
result['title'] = self.schema.title
|
||||||
'alias': self.alias,
|
result['comment'] = self.schema.comment
|
||||||
'convention': self.convention,
|
result['time_update'] = self.schema.time_update
|
||||||
'term': {
|
result['time_create'] = self.schema.time_create
|
||||||
'raw': self.term_raw,
|
result['is_common'] = self.schema.is_common
|
||||||
'resolved': self.term_resolved,
|
result['owner'] = (self.schema.owner.pk if self.schema.owner is not None else None)
|
||||||
'forms': self.term_forms,
|
for cst_data in result['items']:
|
||||||
},
|
cst = Constituenta.objects.get(pk=cst_data['id'])
|
||||||
'definition': {
|
cst_data['convention'] = cst.convention
|
||||||
'formal': self.definition_formal,
|
cst_data['term'] = {
|
||||||
'text': {
|
'raw': cst.term_raw,
|
||||||
'raw': self.definition_raw,
|
'resolved': cst.term_resolved,
|
||||||
'resolved': self.definition_resolved,
|
'forms': cst.term_forms
|
||||||
},
|
}
|
||||||
},
|
cst_data['definition']['text'] = {
|
||||||
|
'raw': cst.definition_raw,
|
||||||
|
'resolved': cst.definition_resolved,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _prepare_request(self) -> dict:
|
||||||
|
result: dict = {
|
||||||
|
'items': []
|
||||||
}
|
}
|
||||||
|
items = self.schema.constituents().order_by('order')
|
||||||
|
for cst in items:
|
||||||
|
result['items'].append({
|
||||||
|
'entityUID': cst.pk,
|
||||||
|
'cstType': cst.cst_type,
|
||||||
|
'alias': cst.alias,
|
||||||
|
'definition': {
|
||||||
|
'formal': cst.definition_formal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _produce_response(self):
|
||||||
|
if self._checked_data is not None:
|
||||||
|
return
|
||||||
|
response = pyconcept.check_schema(json.dumps(self.data))
|
||||||
|
data = json.loads(response)
|
||||||
|
self._checked_data = {
|
||||||
|
'items': []
|
||||||
|
}
|
||||||
|
for cst in data['items']:
|
||||||
|
self._checked_data['items'].append({
|
||||||
|
'id': cst['entityUID'],
|
||||||
|
'cstType': cst['cstType'],
|
||||||
|
'alias': cst['alias'],
|
||||||
|
'definition': {
|
||||||
|
'formal': cst['definition']['formal']
|
||||||
|
},
|
||||||
|
'parse': cst['parse']
|
||||||
|
})
|
||||||
|
|
|
@ -1,35 +1,28 @@
|
||||||
''' Serializers for conceptual schema API. '''
|
''' Serializers for conceptual schema API. '''
|
||||||
import json
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
import pyconcept
|
|
||||||
from .models import Constituenta, RSForm
|
from .models import Constituenta, RSForm
|
||||||
|
|
||||||
|
_CST_TYPE = 'constituenta'
|
||||||
|
_TRS_TYPE = 'rsform'
|
||||||
|
_TRS_VERSION_MIN = 16
|
||||||
|
_TRS_VERSION = 16
|
||||||
|
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
|
||||||
|
|
||||||
|
|
||||||
class FileSerializer(serializers.Serializer):
|
class FileSerializer(serializers.Serializer):
|
||||||
''' Serializer: File input. '''
|
''' Serializer: File input. '''
|
||||||
file = serializers.FileField(allow_empty_file=False)
|
file = serializers.FileField(allow_empty_file=False)
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `create()` call')
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `update()` call')
|
|
||||||
|
|
||||||
|
|
||||||
class ExpressionSerializer(serializers.Serializer):
|
class ExpressionSerializer(serializers.Serializer):
|
||||||
''' Serializer: RSLang expression. '''
|
''' Serializer: RSLang expression. '''
|
||||||
expression = serializers.CharField()
|
expression = serializers.CharField()
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `create()` call')
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
class RSFormMetaSerializer(serializers.ModelSerializer):
|
||||||
raise NotImplementedError('unexpected `update()` call')
|
|
||||||
|
|
||||||
|
|
||||||
class RSFormSerializer(serializers.ModelSerializer):
|
|
||||||
''' Serializer: General purpose RSForm data. '''
|
''' Serializer: General purpose RSForm data. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
|
@ -43,12 +36,6 @@ class RSFormUploadSerializer(serializers.Serializer):
|
||||||
file = serializers.FileField()
|
file = serializers.FileField()
|
||||||
load_metadata = serializers.BooleanField()
|
load_metadata = serializers.BooleanField()
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `create()` call')
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `update()` call')
|
|
||||||
|
|
||||||
|
|
||||||
class RSFormContentsSerializer(serializers.ModelSerializer):
|
class RSFormContentsSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Detailed data for RSForm. '''
|
''' Serializer: Detailed data for RSForm. '''
|
||||||
|
@ -57,35 +44,168 @@ class RSFormContentsSerializer(serializers.ModelSerializer):
|
||||||
model = RSForm
|
model = RSForm
|
||||||
|
|
||||||
def to_representation(self, instance: RSForm):
|
def to_representation(self, instance: RSForm):
|
||||||
result = RSFormSerializer(instance).data
|
result = RSFormMetaSerializer(instance).data
|
||||||
result['items'] = []
|
result['items'] = []
|
||||||
for cst in instance.constituents().order_by('order'):
|
for cst in instance.constituents().order_by('order'):
|
||||||
result['items'].append(ConstituentaSerializer(cst).data)
|
result['items'].append(ConstituentaSerializer(cst).data)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class RSFormDetailsSerlializer(serializers.BaseSerializer):
|
class RSFormTRSSerializer(serializers.Serializer):
|
||||||
''' Serializer: Processed data for RSForm. '''
|
''' Serializer: TRS file production and loading for RSForm. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = RSForm
|
model = RSForm
|
||||||
|
|
||||||
def to_representation(self, instance: RSForm):
|
def to_representation(self, instance: RSForm) -> dict:
|
||||||
trs = pyconcept.check_schema(json.dumps(instance.to_trs()))
|
result = self._prepare_json_rsform(instance)
|
||||||
trs = trs.replace('entityUID', 'id')
|
items = instance.constituents().order_by('order')
|
||||||
result = json.loads(trs)
|
for cst in items:
|
||||||
result['id'] = instance.pk
|
result['items'].append(self._prepare_json_constituenta(cst))
|
||||||
result['time_update'] = instance.time_update
|
|
||||||
result['time_create'] = instance.time_create
|
|
||||||
result['is_common'] = instance.is_common
|
|
||||||
result['owner'] = (instance.owner.pk if instance.owner is not None else None)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def create(self, validated_data):
|
@staticmethod
|
||||||
raise NotImplementedError('unexpected `create()` call')
|
def _prepare_json_rsform(schema: RSForm) -> dict:
|
||||||
|
return {
|
||||||
|
'type': _TRS_TYPE,
|
||||||
|
'title': schema.title,
|
||||||
|
'alias': schema.alias,
|
||||||
|
'comment': schema.comment,
|
||||||
|
'items': [],
|
||||||
|
'claimed': False,
|
||||||
|
'selection': [],
|
||||||
|
'version': _TRS_VERSION,
|
||||||
|
'versionInfo': _TRS_HEADER
|
||||||
|
}
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
@staticmethod
|
||||||
raise NotImplementedError('unexpected `update()` call')
|
def _prepare_json_constituenta(cst: Constituenta) -> dict:
|
||||||
|
return {
|
||||||
|
'entityUID': cst.pk,
|
||||||
|
'type': _CST_TYPE,
|
||||||
|
'cstType': cst.cst_type,
|
||||||
|
'alias': cst.alias,
|
||||||
|
'convention': cst.convention,
|
||||||
|
'term': {
|
||||||
|
'raw': cst.term_raw,
|
||||||
|
'resolved': cst.term_resolved,
|
||||||
|
'forms': cst.term_forms
|
||||||
|
},
|
||||||
|
'definition': {
|
||||||
|
'formal': cst.definition_formal,
|
||||||
|
'text': {
|
||||||
|
'raw': cst.definition_raw,
|
||||||
|
'resolved': cst.definition_resolved
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
result = super().to_internal_value(data)
|
||||||
|
if 'owner' in data:
|
||||||
|
result['owner'] = data['owner']
|
||||||
|
if 'is_common' in data:
|
||||||
|
result['is_common'] = data['is_common']
|
||||||
|
result['items'] = data.get('items', [])
|
||||||
|
if self.context['load_meta']:
|
||||||
|
result['title'] = data.get('title', 'Без названия')
|
||||||
|
result['alias'] = data.get('alias', '')
|
||||||
|
result['comment']= data.get('comment', '')
|
||||||
|
if 'id' in data:
|
||||||
|
result['id'] = data['id']
|
||||||
|
self.instance = RSForm.objects.get(pk=result['id'])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def validate(self, attrs: dict):
|
||||||
|
if 'version' not in self.initial_data \
|
||||||
|
or self.initial_data['version'] < _TRS_VERSION_MIN \
|
||||||
|
or self.initial_data['version'] > _TRS_VERSION:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'version': 'Некорректная версия файла Экстеор. Пересохраните файл в новой версии'
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data: dict) -> RSForm:
|
||||||
|
self.instance = RSForm(
|
||||||
|
owner=validated_data.get('owner', None),
|
||||||
|
alias=validated_data['alias'],
|
||||||
|
title=validated_data['title'],
|
||||||
|
comment=validated_data['comment'],
|
||||||
|
is_common=validated_data['is_common']
|
||||||
|
)
|
||||||
|
self.instance.save()
|
||||||
|
order = 1
|
||||||
|
for cst_data in validated_data['items']:
|
||||||
|
cst = Constituenta(
|
||||||
|
alias=cst_data['alias'],
|
||||||
|
schema=self.instance,
|
||||||
|
order=order,
|
||||||
|
cst_type=cst_data['cstType'],
|
||||||
|
)
|
||||||
|
self._load_cst_texts(cst, cst_data)
|
||||||
|
cst.save()
|
||||||
|
order += 1
|
||||||
|
self.instance.resolve_all_text()
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance: RSForm, validated_data) -> RSForm:
|
||||||
|
if 'alias' in validated_data:
|
||||||
|
instance.alias = validated_data['alias']
|
||||||
|
if 'title' in validated_data:
|
||||||
|
instance.title = validated_data['title']
|
||||||
|
if 'comment' in validated_data:
|
||||||
|
instance.comment = validated_data['comment']
|
||||||
|
|
||||||
|
order = 1
|
||||||
|
prev_constituents = instance.constituents()
|
||||||
|
loaded_ids = set()
|
||||||
|
for cst_data in validated_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.alias = cst_data['alias']
|
||||||
|
cst.cst_type = cst_data['cstType']
|
||||||
|
self._load_cst_texts(cst, cst_data)
|
||||||
|
cst.save()
|
||||||
|
else:
|
||||||
|
cst = Constituenta(
|
||||||
|
alias=cst_data['alias'],
|
||||||
|
schema=instance,
|
||||||
|
order=order,
|
||||||
|
cst_type=cst_data['cstType'],
|
||||||
|
)
|
||||||
|
self._load_cst_texts(cst, cst_data)
|
||||||
|
cst.save()
|
||||||
|
uid = cst.pk
|
||||||
|
loaded_ids.add(uid)
|
||||||
|
order += 1
|
||||||
|
for prev_cst in prev_constituents:
|
||||||
|
if prev_cst.pk not in loaded_ids:
|
||||||
|
prev_cst.delete()
|
||||||
|
|
||||||
|
instance.update_order()
|
||||||
|
instance.resolve_all_text()
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_cst_texts(cst: Constituenta, data: dict):
|
||||||
|
cst.convention = data.get('convention', '')
|
||||||
|
if 'definition' in data:
|
||||||
|
cst.definition_formal = data['definition'].get('formal', '')
|
||||||
|
if 'text' in data['definition']:
|
||||||
|
cst.definition_raw = data['definition']['text'].get('raw', '')
|
||||||
|
else:
|
||||||
|
cst.definition_raw = ''
|
||||||
|
if 'term' in data:
|
||||||
|
cst.term_raw = data['term'].get('raw', '')
|
||||||
|
cst.term_forms = data['term'].get('forms', [])
|
||||||
|
else:
|
||||||
|
cst.term_raw = ''
|
||||||
|
cst.term_forms = []
|
||||||
|
|
||||||
|
|
||||||
class ConstituentaSerializer(serializers.ModelSerializer):
|
class ConstituentaSerializer(serializers.ModelSerializer):
|
||||||
|
@ -116,7 +236,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class StandaloneCstSerializer(serializers.ModelSerializer):
|
class CstStandaloneSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Constituenta in current context. '''
|
''' Serializer: Constituenta in current context. '''
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
|
|
||||||
|
@ -146,7 +266,7 @@ class CstCreateSerializer(serializers.ModelSerializer):
|
||||||
class CstListSerlializer(serializers.Serializer):
|
class CstListSerlializer(serializers.Serializer):
|
||||||
''' Serializer: List of constituents from one origin. '''
|
''' Serializer: List of constituents from one origin. '''
|
||||||
items = serializers.ListField(
|
items = serializers.ListField(
|
||||||
child=StandaloneCstSerializer()
|
child=CstStandaloneSerializer()
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
@ -161,19 +281,7 @@ class CstListSerlializer(serializers.Serializer):
|
||||||
attrs['constituents'] = cstList
|
attrs['constituents'] = cstList
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `create()` call')
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `update()` call')
|
|
||||||
|
|
||||||
|
|
||||||
class CstMoveSerlializer(CstListSerlializer):
|
class CstMoveSerlializer(CstListSerlializer):
|
||||||
''' Serializer: Change constituenta position. '''
|
''' Serializer: Change constituenta position. '''
|
||||||
move_to = serializers.IntegerField()
|
move_to = serializers.IntegerField()
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `create()` call')
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
raise NotImplementedError('unexpected `update()` call')
|
|
||||||
|
|
|
@ -4,3 +4,4 @@ from .t_views import *
|
||||||
from .t_models import *
|
from .t_models import *
|
||||||
from .t_serializers import *
|
from .t_serializers import *
|
||||||
from .t_graph import *
|
from .t_graph import *
|
||||||
|
from .t_utils import *
|
||||||
|
|
|
@ -227,64 +227,30 @@ class TestRSForm(TestCase):
|
||||||
self.assertEqual(x1.order, 2)
|
self.assertEqual(x1.order, 2)
|
||||||
self.assertEqual(x2.order, 1)
|
self.assertEqual(x2.order, 1)
|
||||||
|
|
||||||
def test_to_trs(self):
|
def test_reset_aliases(self):
|
||||||
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
schema = RSForm.objects.create(title='Test')
|
||||||
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
x1 = schema.insert_last('X11', CstType.BASE)
|
||||||
x2 = schema.insert_at(1, 'X2', CstType.BASE)
|
x2 = schema.insert_last('X21', CstType.BASE)
|
||||||
expected = json.loads(
|
d1 = schema.insert_last('D11', CstType.TERM)
|
||||||
f'{{"type": "rsform", "title": "Test", "alias": "KS1", '
|
x1.term_raw = 'человек'
|
||||||
f'"comment": "Test", "items": '
|
x1.term_resolved = 'человек'
|
||||||
f'[{{"entityUID": {x2.id}, "type": "constituenta", "cstType": "basic", "alias": "X2", "convention": "", '
|
d1.convention = 'D11 - cool'
|
||||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
d1.definition_formal = 'X21=X21'
|
||||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}, '
|
d1.term_raw = '@{X21|sing}'
|
||||||
f'{{"entityUID": {x1.id}, "type": "constituenta", "cstType": "basic", "alias": "X1", "convention": "", '
|
d1.definition_raw = '@{X11|datv}'
|
||||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
d1.definition_resolved = 'test'
|
||||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}]}}'
|
d1.save()
|
||||||
)
|
x1.save()
|
||||||
self.assertEqual(schema.to_trs(), expected)
|
|
||||||
|
|
||||||
def test_create_from_trs(self):
|
schema.reset_aliases()
|
||||||
input = json.loads(
|
x1.refresh_from_db()
|
||||||
'{"type": "rsform", "title": "Test", "alias": "KS1", '
|
|
||||||
'"comment": "Test", "items": '
|
|
||||||
'[{"entityUID": 1337, "type": "constituenta", "cstType": "basic", "alias": "X1", "convention": "", '
|
|
||||||
'"term": {"raw": "", "resolved": ""}, '
|
|
||||||
'"definition": {"formal": "123", "text": {"raw": "", "resolved": ""}}}, '
|
|
||||||
'{"entityUID": 55, "type": "constituenta", "cstType": "basic", "alias": "X2", "convention": "", '
|
|
||||||
'"term": {"raw": "", "resolved": ""}, '
|
|
||||||
'"definition": {"formal": "", "text": {"raw": "", "resolved": ""}}}]}'
|
|
||||||
)
|
|
||||||
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')
|
|
||||||
self.assertEqual(schema.is_common, False)
|
|
||||||
constituents = schema.constituents().order_by('order')
|
|
||||||
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": "@{X1|datv}", "resolved": "t4"}}}]}'
|
|
||||||
)
|
|
||||||
schema.load_trs(input, sync_metadata=True, skip_update=True)
|
|
||||||
x2.refresh_from_db()
|
x2.refresh_from_db()
|
||||||
self.assertEqual(schema.constituents().count(), 1)
|
d1.refresh_from_db()
|
||||||
self.assertEqual(schema.title, input['title'])
|
|
||||||
self.assertEqual(schema.alias, input['alias'])
|
self.assertEqual(x1.alias, 'X1')
|
||||||
self.assertEqual(schema.comment, input['comment'])
|
self.assertEqual(x2.alias, 'X2')
|
||||||
self.assertEqual(x2.alias, input['items'][0]['alias'])
|
self.assertEqual(d1.alias, 'D1')
|
||||||
self.assertEqual(x2.convention, input['items'][0]['convention'])
|
self.assertEqual(d1.convention, 'D1 - cool')
|
||||||
self.assertEqual(x2.term_raw, input['items'][0]['term']['raw'])
|
self.assertEqual(d1.term_raw, '@{X2|sing}')
|
||||||
self.assertEqual(x2.term_resolved, input['items'][0]['term']['raw'])
|
self.assertEqual(d1.definition_raw, '@{X1|datv}')
|
||||||
self.assertEqual(x2.definition_formal, input['items'][0]['definition']['formal'])
|
self.assertEqual(d1.definition_resolved, 'test')
|
||||||
self.assertEqual(x2.definition_raw, input['items'][0]['definition']['text']['raw'])
|
|
||||||
self.assertEqual(x2.definition_resolved, input['items'][0]['term']['raw'])
|
|
||||||
|
|
16
rsconcept/backend/apps/rsform/tests/t_utils.py
Normal file
16
rsconcept/backend/apps/rsform/tests/t_utils.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
''' Unit tests: utils. '''
|
||||||
|
import unittest
|
||||||
|
import re
|
||||||
|
|
||||||
|
from apps.rsform.utils import apply_mapping_pattern
|
||||||
|
|
||||||
|
|
||||||
|
class TestUtils(unittest.TestCase):
|
||||||
|
''' Test various utilitiy functions. '''
|
||||||
|
def test_apply_mapping_patter(self):
|
||||||
|
mapping = {'X101': 'X20'}
|
||||||
|
pattern = re.compile(r'(X[0-9]+)')
|
||||||
|
self.assertEqual(apply_mapping_pattern('', mapping, pattern), '')
|
||||||
|
self.assertEqual(apply_mapping_pattern('X20', mapping, pattern), 'X20')
|
||||||
|
self.assertEqual(apply_mapping_pattern('X101', mapping, pattern), 'X20')
|
||||||
|
self.assertEqual(apply_mapping_pattern('asdf X101 asdf', mapping, pattern), 'asdf X20 asdf')
|
|
@ -1,6 +1,7 @@
|
||||||
''' Utility functions '''
|
''' Utility functions '''
|
||||||
import json
|
import json
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
import re
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
|
@ -35,13 +36,24 @@ def read_trs(file) -> dict:
|
||||||
|
|
||||||
def write_trs(json_data: dict) -> bytes:
|
def write_trs(json_data: dict) -> bytes:
|
||||||
''' Write json data to TRS file including version info '''
|
''' Write json data to TRS file including version info '''
|
||||||
json_data["claimed"] = False
|
|
||||||
json_data["selection"] = []
|
|
||||||
json_data["version"] = 16
|
|
||||||
json_data["versionInfo"] = "Exteor 4.8.13.1000 - 30/05/2022"
|
|
||||||
|
|
||||||
content = BytesIO()
|
content = BytesIO()
|
||||||
data = json.dumps(json_data, indent=4, ensure_ascii=False)
|
data = json.dumps(json_data, indent=4, ensure_ascii=False)
|
||||||
with ZipFile(content, 'w') as archive:
|
with ZipFile(content, 'w') as archive:
|
||||||
archive.writestr('document.json', data=data)
|
archive.writestr('document.json', data=data)
|
||||||
return content.getvalue()
|
return content.getvalue()
|
||||||
|
|
||||||
|
def apply_mapping_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str:
|
||||||
|
''' Apply mapping to matching in regular expression patter subgroup 1. '''
|
||||||
|
if text == '' or pattern == '':
|
||||||
|
return text
|
||||||
|
pos_input: int = 0
|
||||||
|
output: str = ''
|
||||||
|
for segment in re.finditer(pattern, text):
|
||||||
|
entity = segment.group(1)
|
||||||
|
if entity in mapping:
|
||||||
|
output += text[pos_input : segment.start(1)]
|
||||||
|
output += mapping[entity]
|
||||||
|
output += text[segment.end(1) : segment.end(0)]
|
||||||
|
pos_input = segment.end(0)
|
||||||
|
output += text[pos_input : len(text)]
|
||||||
|
return output
|
||||||
|
|
|
@ -17,7 +17,7 @@ from . import utils
|
||||||
class LibraryView(generics.ListAPIView):
|
class LibraryView(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,)
|
||||||
serializer_class = serializers.RSFormSerializer
|
serializer_class = serializers.RSFormMetaSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
@ -45,7 +45,7 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView):
|
||||||
class RSFormViewSet(viewsets.ModelViewSet):
|
class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
''' Endpoint: RSForm operations. '''
|
''' Endpoint: RSForm operations. '''
|
||||||
queryset = models.RSForm.objects.all()
|
queryset = models.RSForm.objects.all()
|
||||||
serializer_class = serializers.RSFormSerializer
|
serializer_class = serializers.RSFormMetaSerializer
|
||||||
|
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
filterset_fields = ['owner', 'is_common']
|
filterset_fields = ['owner', 'is_common']
|
||||||
|
@ -80,10 +80,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
new_cst = schema.create_cst(data, data['insert_after'] if 'insert_after' in data else None)
|
new_cst = schema.create_cst(data, data['insert_after'] if 'insert_after' in data else None)
|
||||||
schema.refresh_from_db()
|
schema.refresh_from_db()
|
||||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
|
||||||
response = Response(status=201, data={
|
response = Response(status=201, data={
|
||||||
'new_cst': serializers.ConstituentaSerializer(new_cst).data,
|
'new_cst': serializers.ConstituentaSerializer(new_cst).data,
|
||||||
'schema': outSerializer.data
|
'schema': models.PyConceptAdapter(schema).full()
|
||||||
})
|
})
|
||||||
response['Location'] = new_cst.get_absolute_url()
|
response['Location'] = new_cst.get_absolute_url()
|
||||||
return response
|
return response
|
||||||
|
@ -96,8 +95,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.delete_cst(serializer.validated_data['constituents'])
|
schema.delete_cst(serializer.validated_data['constituents'])
|
||||||
schema.refresh_from_db()
|
schema.refresh_from_db()
|
||||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
return Response(status=202, data=models.PyConceptAdapter(schema).full())
|
||||||
return Response(status=202, data=outSerializer.data)
|
|
||||||
|
|
||||||
@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):
|
||||||
|
@ -107,17 +105,14 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
||||||
schema.refresh_from_db()
|
schema.refresh_from_db()
|
||||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
return Response(status=200, data=models.PyConceptAdapter(schema).full())
|
||||||
return Response(status=200, data=outSerializer.data)
|
|
||||||
|
|
||||||
@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. '''
|
||||||
schema = self._get_schema()
|
schema = self._get_schema()
|
||||||
result = json.loads(pyconcept.reset_aliases(json.dumps(schema.to_trs())))
|
schema.reset_aliases()
|
||||||
schema.load_trs(data=result, sync_metadata=False, skip_update=True)
|
return Response(status=200, data=models.PyConceptAdapter(schema).full())
|
||||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
|
||||||
return Response(status=200, data=outSerializer.data)
|
|
||||||
|
|
||||||
@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):
|
||||||
|
@ -127,25 +122,30 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
schema = self._get_schema()
|
schema = self._get_schema()
|
||||||
load_metadata = serializer.validated_data['load_metadata']
|
load_metadata = serializer.validated_data['load_metadata']
|
||||||
data = utils.read_trs(request.FILES['file'].file)
|
data = utils.read_trs(request.FILES['file'].file)
|
||||||
schema.load_trs(data, load_metadata, skip_update=False)
|
data['id'] = schema.pk
|
||||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
|
||||||
return Response(status=200, data=outSerializer.data)
|
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
schema = serializer.save()
|
||||||
|
return Response(status=200, data=models.PyConceptAdapter(schema).full())
|
||||||
|
|
||||||
@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):
|
||||||
''' Endpoint: Clone RSForm constituents and create new schema using new metadata. '''
|
''' Endpoint: Clone RSForm constituents and create new schema using new metadata. '''
|
||||||
serializer = serializers.RSFormSerializer(data=request.data)
|
serializer = serializers.RSFormMetaSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
new_schema = models.RSForm.objects.create(
|
|
||||||
title=serializer.validated_data['title'],
|
clone_data = serializers.RSFormTRSSerializer(self._get_schema()).data
|
||||||
owner=self.request.user,
|
clone_data['owner'] = self.request.user
|
||||||
alias=serializer.validated_data.get('alias', ''),
|
clone_data['title'] = serializer.validated_data['title']
|
||||||
comment=serializer.validated_data.get('comment', ''),
|
clone_data['alias'] = serializer.validated_data.get('alias', '')
|
||||||
is_common=serializer.validated_data.get('is_common', False),
|
clone_data['comment'] = serializer.validated_data.get('comment', '')
|
||||||
)
|
clone_data['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)
|
clone = serializers.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
|
||||||
return Response(status=201, data=outSerializer.data)
|
clone.is_valid(raise_exception=True)
|
||||||
|
new_schema = clone.save()
|
||||||
|
return Response(status=201, data=models.PyConceptAdapter(new_schema).full())
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def claim(self, request, pk=None):
|
def claim(self, request, pk=None):
|
||||||
|
@ -156,7 +156,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
else:
|
else:
|
||||||
schema.owner = self.request.user
|
schema.owner = self.request.user
|
||||||
schema.save()
|
schema.save()
|
||||||
return Response(status=200, data=serializers.RSFormSerializer(schema).data)
|
return Response(status=200, data=serializers.RSFormMetaSerializer(schema).data)
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def contents(self, request, pk):
|
def contents(self, request, pk):
|
||||||
|
@ -166,25 +166,25 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
@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. '''
|
''' Endpoint: Detailed schema view including statuses and parse. '''
|
||||||
schema = self._get_schema()
|
schema = self._get_schema()
|
||||||
serializer = serializers.RSFormDetailsSerlializer(schema)
|
serializer = models.PyConceptAdapter(schema)
|
||||||
return Response(serializer.data)
|
return Response(serializer.full())
|
||||||
|
|
||||||
@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. '''
|
||||||
schema = self._get_schema().to_trs()
|
schema = models.PyConceptAdapter(self._get_schema())
|
||||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
expression = serializer.validated_data['expression']
|
expression = serializer.validated_data['expression']
|
||||||
result = pyconcept.check_expression(json.dumps(schema), expression)
|
result = pyconcept.check_expression(json.dumps(schema.data), expression)
|
||||||
return Response(json.loads(result))
|
return Response(json.loads(result))
|
||||||
|
|
||||||
@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. '''
|
||||||
schema = self._get_schema().to_trs()
|
schema = serializers.RSFormTRSSerializer(self._get_schema()).data
|
||||||
trs = utils.write_trs(schema)
|
trs = utils.write_trs(schema)
|
||||||
filename = self._get_schema().alias
|
filename = self._get_schema().alias
|
||||||
if filename == '' or not filename.isascii():
|
if filename == '' or not filename.isascii():
|
||||||
|
@ -207,8 +207,11 @@ class TrsImportView(views.APIView):
|
||||||
owner = self.request.user
|
owner = self.request.user
|
||||||
if owner.is_anonymous:
|
if owner.is_anonymous:
|
||||||
owner = None
|
owner = None
|
||||||
schema = models.RSForm.create_from_trs(owner, data)
|
_prepare_rsform_data(data, request, owner)
|
||||||
result = serializers.RSFormSerializer(schema)
|
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': True})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
schema = serializer.save()
|
||||||
|
result = serializers.RSFormMetaSerializer(schema)
|
||||||
return Response(status=201, data=result.data)
|
return Response(status=201, data=result.data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -219,7 +222,7 @@ def create_rsform(request):
|
||||||
if owner.is_anonymous:
|
if owner.is_anonymous:
|
||||||
owner = None
|
owner = None
|
||||||
if 'file' not in request.FILES:
|
if 'file' not in request.FILES:
|
||||||
serializer = serializers.RSFormSerializer(data=request.data)
|
serializer = serializers.RSFormMetaSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema = models.RSForm.objects.create(
|
schema = models.RSForm.objects.create(
|
||||||
title=serializer.validated_data['title'],
|
title=serializer.validated_data['title'],
|
||||||
|
@ -230,21 +233,27 @@ def create_rsform(request):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = utils.read_trs(request.FILES['file'].file)
|
data = utils.read_trs(request.FILES['file'].file)
|
||||||
if 'title' in request.data and request.data['title'] != '':
|
_prepare_rsform_data(data, request, owner)
|
||||||
data['title'] = request.data['title']
|
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': True})
|
||||||
if data['title'] == '':
|
serializer.is_valid(raise_exception=True)
|
||||||
data['title'] = 'Без названия ' + request.FILES['file'].fileName
|
schema = serializer.save()
|
||||||
if 'alias' in request.data and request.data['alias'] != '':
|
result = serializers.RSFormMetaSerializer(schema)
|
||||||
data['alias'] = request.data['alias']
|
|
||||||
if 'comment' in request.data and request.data['comment'] != '':
|
|
||||||
data['comment'] = request.data['comment']
|
|
||||||
is_common = True
|
|
||||||
if 'is_common' in request.data:
|
|
||||||
is_common = request.data['is_common'] == 'true'
|
|
||||||
schema = models.RSForm.create_from_trs(owner, data, is_common)
|
|
||||||
result = serializers.RSFormSerializer(schema)
|
|
||||||
return Response(status=201, data=result.data)
|
return Response(status=201, data=result.data)
|
||||||
|
|
||||||
|
def _prepare_rsform_data(data: dict, request, owner: models.User):
|
||||||
|
if 'title' in request.data and request.data['title'] != '':
|
||||||
|
data['title'] = request.data['title']
|
||||||
|
if data['title'] == '':
|
||||||
|
data['title'] = 'Без названия ' + request.FILES['file'].fileName
|
||||||
|
if 'alias' in request.data and request.data['alias'] != '':
|
||||||
|
data['alias'] = request.data['alias']
|
||||||
|
if 'comment' in request.data and request.data['comment'] != '':
|
||||||
|
data['comment'] = request.data['comment']
|
||||||
|
is_common = True
|
||||||
|
if 'is_common' in request.data:
|
||||||
|
is_common = request.data['is_common'] == 'true'
|
||||||
|
data['is_common'] = is_common
|
||||||
|
data['owner'] = owner
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def parse_expression(request):
|
def parse_expression(request):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
''' Tests. '''
|
''' Tests. '''
|
||||||
# flake8: noqa
|
|
||||||
from .t_views import *
|
from .t_views import *
|
||||||
from .t_serializers import *
|
from .t_serializers import *
|
||||||
|
|
|
@ -43,7 +43,7 @@ function App () {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='overflow-auto' style={{maxHeight: scrollWindowSize}}>
|
<div className='overflow-auto' style={{maxHeight: scrollWindowSize}}>
|
||||||
<main className='px-2' style={{minHeight: mainSize}}>
|
<main style={{minHeight: mainSize}}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={ <HomePage/>} />
|
<Route path='/' element={ <HomePage/>} />
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
interface SubmitButtonProps {
|
interface SubmitButtonProps
|
||||||
text: string
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'> {
|
||||||
|
text?: string
|
||||||
|
tooltip?: string
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
disabled?: boolean
|
|
||||||
icon?: React.ReactNode
|
icon?: React.ReactNode
|
||||||
|
widthClass?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: SubmitButtonProps) {
|
function SubmitButton({
|
||||||
|
text = 'ОК', icon, disabled, tooltip, loading,
|
||||||
|
widthClass = 'w-fit h-fit'
|
||||||
|
}: SubmitButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button type='submit'
|
<button type='submit'
|
||||||
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${loading ? ' cursor-progress' : ''}`}
|
title={tooltip}
|
||||||
|
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${widthClass} ${loading ? ' cursor-progress' : ''}`}
|
||||||
disabled={disabled ?? loading}
|
disabled={disabled ?? loading}
|
||||||
>
|
>
|
||||||
{icon && <span>{icon}</span>}
|
{icon && <span>{icon}</span>}
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface TextURLProps {
|
||||||
|
|
||||||
function TextURL({ text, href }: TextURLProps) {
|
function TextURL({ text, href }: TextURLProps) {
|
||||||
return (
|
return (
|
||||||
<Link className='font-bold hover:underline clr-text' to={href}>
|
<Link className='font-bold hover:underline text-url' to={href}>
|
||||||
{text}
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -68,8 +68,9 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
pointerOnHover
|
pointerOnHover
|
||||||
|
|
||||||
noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>
|
noDataComponent={
|
||||||
<p>Список схем пуст</p>
|
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[10rem]'>
|
||||||
|
<p><b>Список схем пуст</b></p>
|
||||||
<p>
|
<p>
|
||||||
<TextURL text='Создать схему' href='/rsform-create'/>
|
<TextURL text='Создать схему' href='/rsform-create'/>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
@ -79,7 +80,7 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
|
||||||
<b>Очистить фильтр</b>
|
<b>Очистить фильтр</b>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</span>}
|
</div>}
|
||||||
|
|
||||||
pagination
|
pagination
|
||||||
paginationPerPage={50}
|
paginationPerPage={50}
|
||||||
|
|
|
@ -39,7 +39,7 @@ function LoginPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-2'> { user
|
<div className='w-full py-4'> { user
|
||||||
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
||||||
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[21rem]'>
|
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[21rem]'>
|
||||||
<TextInput id='username'
|
<TextInput id='username'
|
||||||
|
@ -58,11 +58,15 @@ function LoginPage() {
|
||||||
onChange={event => { setPassword(event.target.value); }}
|
onChange={event => { setPassword(event.target.value); }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex items-center justify-between mt-4'>
|
<div className='flex justify-center w-full gap-2 mt-4'>
|
||||||
<SubmitButton text='Вход' loading={loading}/>
|
<SubmitButton
|
||||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
text='Вход'
|
||||||
|
widthClass='w-[7rem]'
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2'>
|
<div className='flex flex-col mt-2 text-sm'>
|
||||||
|
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||||
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
||||||
</div>
|
</div>
|
||||||
{ error && <BackendError error={error} />}
|
{ error && <BackendError error={error} />}
|
||||||
|
|
|
@ -217,7 +217,7 @@ function RSTabs() {
|
||||||
defaultFocus={true}
|
defaultFocus={true}
|
||||||
selectedTabClassName='font-bold'
|
selectedTabClassName='font-bold'
|
||||||
>
|
>
|
||||||
<TabList className='flex items-start select-none w-fit clr-bg-pop'>
|
<TabList className='flex items-start px-2 select-none w-fit clr-bg-pop'>
|
||||||
<RSTabsMenu
|
<RSTabsMenu
|
||||||
onDestroy={onDestroySchema}
|
onDestroy={onDestroySchema}
|
||||||
showCloneDialog={() => setShowClone(true)}
|
showCloneDialog={() => setShowClone(true)}
|
||||||
|
@ -232,7 +232,7 @@ function RSTabs() {
|
||||||
<ConceptTab>Граф термов</ConceptTab>
|
<ConceptTab>Граф термов</ConceptTab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanel className='flex items-start w-full gap-2'>
|
<TabPanel className='flex items-start w-full gap-2 px-2'>
|
||||||
<EditorRSForm
|
<EditorRSForm
|
||||||
onDestroy={onDestroySchema}
|
onDestroy={onDestroySchema}
|
||||||
/>
|
/>
|
||||||
|
@ -247,7 +247,7 @@ function RSTabs() {
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel className='pl-2'>
|
||||||
<EditorConstituenta
|
<EditorConstituenta
|
||||||
activeID={activeID}
|
activeID={activeID}
|
||||||
onOpenEdit={onOpenCst}
|
onOpenEdit={onOpenCst}
|
||||||
|
@ -257,7 +257,7 @@ function RSTabs() {
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel className='pl-2'>
|
||||||
<EditorTermGraph
|
<EditorTermGraph
|
||||||
onOpenEdit={onOpenCst}
|
onOpenEdit={onOpenCst}
|
||||||
onCreateCst={promptCreateCst}
|
onCreateCst={promptCreateCst}
|
||||||
|
|
|
@ -21,7 +21,7 @@ function DependencyModePicker({ value, onChange }: DependencyModePickerProps) {
|
||||||
}, [pickerMenu, onChange]);
|
}, [pickerMenu, onChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={pickerMenu.ref}>
|
<div ref={pickerMenu.ref} className='h-full'>
|
||||||
<span
|
<span
|
||||||
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|
|
@ -21,7 +21,7 @@ function MatchModePicker({ value, onChange }: MatchModePickerProps) {
|
||||||
}, [pickerMenu, onChange]);
|
}, [pickerMenu, onChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={pickerMenu.ref}>
|
<div ref={pickerMenu.ref} className='h-full'>
|
||||||
<span
|
<span
|
||||||
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|
|
@ -152,13 +152,13 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
}, [noNavigation, baseHeight]);
|
}, [noNavigation, baseHeight]);
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'>
|
<div className='sticky top-0 left-0 right-0 z-10 flex items-start justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'>
|
||||||
<MatchModePicker
|
<MatchModePicker
|
||||||
value={filterMatch}
|
value={filterMatch}
|
||||||
onChange={setFilterMatch}
|
onChange={setFilterMatch}
|
||||||
/>
|
/>
|
||||||
<input type='text'
|
<input type='text'
|
||||||
className='w-full px-2 bg-white outline-none select-none hover:text-clip clr-bg-pop clr-border'
|
className='w-full px-2 bg-white outline-none select-none hover:text-clip clr-bg-pop'
|
||||||
placeholder='наберите текст фильтра'
|
placeholder='наберите текст фильтра'
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={event => setFilterText(event.target.value)}
|
onChange={event => setFilterText(event.target.value)}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user