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,
|
||||
missing-function-docstring,
|
||||
attribute-defined-outside-init,
|
||||
ungrouped-imports
|
||||
ungrouped-imports,
|
||||
abstract-method
|
||||
|
||||
# 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
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.db import migrations
|
|||
|
||||
from apps.rsform import utils
|
||||
from apps.rsform.models import RSForm
|
||||
from apps.rsform.serializers import RSFormTRSSerializer
|
||||
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 file in files:
|
||||
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):
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
''' Models: RSForms for conceptual schemas. '''
|
||||
import json
|
||||
from typing import Iterable, Optional
|
||||
import pyconcept
|
||||
from copy import deepcopy
|
||||
import re
|
||||
from typing import Iterable, Optional, cast
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
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.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
import pyconcept
|
||||
from apps.users.models import User
|
||||
from cctext import Resolver, Entity, extract_entities
|
||||
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):
|
||||
|
@ -37,6 +46,26 @@ class Syntax(TextChoices):
|
|||
def _empty_forms():
|
||||
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):
|
||||
''' RSForm is a math form of capturing conceptual schema '''
|
||||
|
@ -134,7 +163,7 @@ class RSForm(Model):
|
|||
alias=alias,
|
||||
cst_type=insert_type
|
||||
)
|
||||
self._update_order()
|
||||
self.update_order()
|
||||
self.save()
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
@ -151,7 +180,7 @@ class RSForm(Model):
|
|||
alias=alias,
|
||||
cst_type=insert_type
|
||||
)
|
||||
self._update_order()
|
||||
self.update_order()
|
||||
self.save()
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
@ -177,7 +206,7 @@ class RSForm(Model):
|
|||
count_moved += 1
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
self._update_order()
|
||||
self.update_order()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -185,8 +214,8 @@ class RSForm(Model):
|
|||
''' Delete multiple constituents. Do not check if listCst are from this schema '''
|
||||
for cst in listCst:
|
||||
cst.delete()
|
||||
self._update_order()
|
||||
self._resolve_all_text()
|
||||
self.update_order()
|
||||
self.resolve_all_text()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -209,89 +238,61 @@ class RSForm(Model):
|
|||
cst.refresh_from_db()
|
||||
return cst
|
||||
|
||||
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 reset_aliases(self):
|
||||
''' Recreate all aliases based on cst order. '''
|
||||
mapping = self._create_reset_mapping()
|
||||
self._apply_mapping(mapping)
|
||||
|
||||
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
|
||||
def load_trs(self, data: dict, sync_metadata: bool, skip_update: bool):
|
||||
if sync_metadata:
|
||||
self.title = data.get('title', 'Без названия')
|
||||
self.alias = data.get('alias', '')
|
||||
self.comment = data.get('comment', '')
|
||||
order = 1
|
||||
prev_constituents = self.constituents()
|
||||
loaded_ids = set()
|
||||
for cst_data in data['items']:
|
||||
uid = int(cst_data['entityUID'])
|
||||
if prev_constituents.filter(pk=uid).exists():
|
||||
cst: Constituenta = prev_constituents.get(pk=uid)
|
||||
cst.order = order
|
||||
cst.load_trs(cst_data)
|
||||
def _apply_mapping(self, mapping: dict[str, str]):
|
||||
cst_list = self.constituents().order_by('order')
|
||||
for cst in cst_list:
|
||||
modified = False
|
||||
if cst.alias in mapping:
|
||||
modified = True
|
||||
cst.alias = mapping[cst.alias]
|
||||
expression = apply_mapping_pattern(cst.definition_formal, mapping, _GLOBAL_ID_PATTERN)
|
||||
if expression != cst.definition_formal:
|
||||
modified = True
|
||||
cst.definition_formal = expression
|
||||
convention = apply_mapping_pattern(cst.convention, mapping, _GLOBAL_ID_PATTERN)
|
||||
if convention != cst.convention:
|
||||
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()
|
||||
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
|
||||
def _update_order(self):
|
||||
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
|
||||
def update_order(self):
|
||||
''' Update constituents order. '''
|
||||
checked = PyConceptAdapter(self).basic()
|
||||
update_list = self.constituents().only('id', 'order')
|
||||
if len(checked['items']) != update_list.count():
|
||||
raise ValidationError('Invalid constituents count')
|
||||
order = 1
|
||||
for cst in checked['items']:
|
||||
cst_id = cst['entityUID']
|
||||
cst_id = cst['id']
|
||||
for oldCst in update_list:
|
||||
if oldCst.pk == cst_id:
|
||||
oldCst.order = order
|
||||
|
@ -300,14 +301,8 @@ class RSForm(Model):
|
|||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
|
||||
@transaction.atomic
|
||||
def _create_items_from_trs(self, items):
|
||||
order = 1
|
||||
for cst in items:
|
||||
cst_object = Constituenta.create_from_trs(cst, self, order)
|
||||
cst_object.save()
|
||||
order += 1
|
||||
|
||||
def _resolve_all_text(self):
|
||||
def resolve_all_text(self):
|
||||
''' Trigger reference resolution for all texts. '''
|
||||
graph_terms = self._term_graph()
|
||||
resolver = Resolver({})
|
||||
for alias in graph_terms.topological_order():
|
||||
|
@ -323,6 +318,19 @@ class RSForm(Model):
|
|||
cst.definition_resolved = resolved
|
||||
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:
|
||||
result = Graph()
|
||||
cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order')
|
||||
|
@ -413,8 +421,8 @@ class Constituenta(Model):
|
|||
''' URL access. '''
|
||||
return reverse('constituenta-detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return self.alias
|
||||
def __str__(self) -> str:
|
||||
return f'{self.alias}'
|
||||
|
||||
def set_term_resolved(self, new_term: str):
|
||||
''' Set term and reset forms if needed. '''
|
||||
|
@ -423,61 +431,83 @@ class Constituenta(Model):
|
|||
self.term_resolved = new_term
|
||||
self.term_forms = []
|
||||
|
||||
@staticmethod
|
||||
def create_from_trs(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
||||
''' Create constituenta from TRS json '''
|
||||
cst = Constituenta(
|
||||
alias=data['alias'],
|
||||
schema=schema,
|
||||
order=order,
|
||||
cst_type=data['cstType'],
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
cst._load_texts(data)
|
||||
return cst
|
||||
class PyConceptAdapter:
|
||||
''' RSForm adapter for interacting with pyconcept module. '''
|
||||
def __init__(self, instance: RSForm):
|
||||
self.schema = instance
|
||||
self.data = self._prepare_request()
|
||||
self._checked_data: Optional[dict] = None
|
||||
|
||||
def load_trs(self, data: dict):
|
||||
''' Load data from TRS json '''
|
||||
self.alias = data['alias']
|
||||
self.cst_type = data['cstType']
|
||||
self._load_texts(data)
|
||||
def basic(self) -> dict:
|
||||
''' Check RSForm and return check results.
|
||||
Warning! Does not include texts. '''
|
||||
self._produce_response()
|
||||
if self._checked_data is None:
|
||||
raise ValueError('Invalid data response from pyconcept')
|
||||
return self._checked_data
|
||||
|
||||
def _load_texts(self, data: dict):
|
||||
self.convention = data.get('convention', '')
|
||||
if 'definition' in data:
|
||||
self.definition_formal = data['definition'].get('formal', '')
|
||||
if 'text' in data['definition']:
|
||||
self.definition_raw = data['definition']['text'].get('raw', '')
|
||||
self.definition_resolved = data['definition']['text'].get('resolved', '')
|
||||
else:
|
||||
self.definition_raw = ''
|
||||
self.definition_resolved = ''
|
||||
if 'term' in data:
|
||||
self.term_raw = data['term'].get('raw', '')
|
||||
self.term_resolved = data['term'].get('resolved', '')
|
||||
self.term_forms = data['term'].get('forms', [])
|
||||
else:
|
||||
self.term_raw = ''
|
||||
self.term_resolved = ''
|
||||
self.term_forms = []
|
||||
def full(self) -> dict:
|
||||
''' Check RSForm and return check results including initial texts. '''
|
||||
self._produce_response()
|
||||
if self._checked_data is None:
|
||||
raise ValueError('Invalid data response from pyconcept')
|
||||
return self._complete_rsform_details(self._checked_data)
|
||||
|
||||
def to_trs(self) -> dict:
|
||||
return {
|
||||
'entityUID': self.pk,
|
||||
'type': 'constituenta',
|
||||
'cstType': self.cst_type,
|
||||
'alias': self.alias,
|
||||
'convention': self.convention,
|
||||
'term': {
|
||||
'raw': self.term_raw,
|
||||
'resolved': self.term_resolved,
|
||||
'forms': self.term_forms,
|
||||
},
|
||||
'definition': {
|
||||
'formal': self.definition_formal,
|
||||
'text': {
|
||||
'raw': self.definition_raw,
|
||||
'resolved': self.definition_resolved,
|
||||
},
|
||||
},
|
||||
def _complete_rsform_details(self, data: dict) -> dict:
|
||||
result = deepcopy(data)
|
||||
result['id'] = self.schema.pk
|
||||
result['alias'] = self.schema.alias
|
||||
result['title'] = self.schema.title
|
||||
result['comment'] = self.schema.comment
|
||||
result['time_update'] = self.schema.time_update
|
||||
result['time_create'] = self.schema.time_create
|
||||
result['is_common'] = self.schema.is_common
|
||||
result['owner'] = (self.schema.owner.pk if self.schema.owner is not None else None)
|
||||
for cst_data in result['items']:
|
||||
cst = Constituenta.objects.get(pk=cst_data['id'])
|
||||
cst_data['convention'] = cst.convention
|
||||
cst_data['term'] = {
|
||||
'raw': cst.term_raw,
|
||||
'resolved': cst.term_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. '''
|
||||
import json
|
||||
from typing import Optional
|
||||
from rest_framework import serializers
|
||||
from django.db import transaction
|
||||
|
||||
import pyconcept
|
||||
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):
|
||||
''' Serializer: File input. '''
|
||||
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):
|
||||
''' Serializer: RSLang expression. '''
|
||||
expression = serializers.CharField()
|
||||
|
||||
def create(self, validated_data):
|
||||
raise NotImplementedError('unexpected `create()` call')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
raise NotImplementedError('unexpected `update()` call')
|
||||
|
||||
|
||||
class RSFormSerializer(serializers.ModelSerializer):
|
||||
class RSFormMetaSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: General purpose RSForm data. '''
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
|
@ -43,12 +36,6 @@ class RSFormUploadSerializer(serializers.Serializer):
|
|||
file = serializers.FileField()
|
||||
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):
|
||||
''' Serializer: Detailed data for RSForm. '''
|
||||
|
@ -57,35 +44,168 @@ class RSFormContentsSerializer(serializers.ModelSerializer):
|
|||
model = RSForm
|
||||
|
||||
def to_representation(self, instance: RSForm):
|
||||
result = RSFormSerializer(instance).data
|
||||
result = RSFormMetaSerializer(instance).data
|
||||
result['items'] = []
|
||||
for cst in instance.constituents().order_by('order'):
|
||||
result['items'].append(ConstituentaSerializer(cst).data)
|
||||
return result
|
||||
|
||||
|
||||
class RSFormDetailsSerlializer(serializers.BaseSerializer):
|
||||
''' Serializer: Processed data for RSForm. '''
|
||||
class RSFormTRSSerializer(serializers.Serializer):
|
||||
''' Serializer: TRS file production and loading for RSForm. '''
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = RSForm
|
||||
|
||||
def to_representation(self, instance: RSForm):
|
||||
trs = pyconcept.check_schema(json.dumps(instance.to_trs()))
|
||||
trs = trs.replace('entityUID', 'id')
|
||||
result = json.loads(trs)
|
||||
result['id'] = instance.pk
|
||||
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)
|
||||
def to_representation(self, instance: RSForm) -> dict:
|
||||
result = self._prepare_json_rsform(instance)
|
||||
items = instance.constituents().order_by('order')
|
||||
for cst in items:
|
||||
result['items'].append(self._prepare_json_constituenta(cst))
|
||||
return result
|
||||
|
||||
def create(self, validated_data):
|
||||
raise NotImplementedError('unexpected `create()` call')
|
||||
@staticmethod
|
||||
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):
|
||||
raise NotImplementedError('unexpected `update()` call')
|
||||
@staticmethod
|
||||
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):
|
||||
|
@ -116,7 +236,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
|
|||
return result
|
||||
|
||||
|
||||
class StandaloneCstSerializer(serializers.ModelSerializer):
|
||||
class CstStandaloneSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: Constituenta in current context. '''
|
||||
id = serializers.IntegerField()
|
||||
|
||||
|
@ -146,7 +266,7 @@ class CstCreateSerializer(serializers.ModelSerializer):
|
|||
class CstListSerlializer(serializers.Serializer):
|
||||
''' Serializer: List of constituents from one origin. '''
|
||||
items = serializers.ListField(
|
||||
child=StandaloneCstSerializer()
|
||||
child=CstStandaloneSerializer()
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
|
@ -161,19 +281,7 @@ class CstListSerlializer(serializers.Serializer):
|
|||
attrs['constituents'] = cstList
|
||||
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):
|
||||
''' Serializer: Change constituenta position. '''
|
||||
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_serializers import *
|
||||
from .t_graph import *
|
||||
from .t_utils import *
|
||||
|
|
|
@ -227,64 +227,30 @@ class TestRSForm(TestCase):
|
|||
self.assertEqual(x1.order, 2)
|
||||
self.assertEqual(x2.order, 1)
|
||||
|
||||
def test_to_trs(self):
|
||||
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
||||
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
||||
x2 = schema.insert_at(1, 'X2', CstType.BASE)
|
||||
expected = json.loads(
|
||||
f'{{"type": "rsform", "title": "Test", "alias": "KS1", '
|
||||
f'"comment": "Test", "items": '
|
||||
f'[{{"entityUID": {x2.id}, "type": "constituenta", "cstType": "basic", "alias": "X2", "convention": "", '
|
||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}, '
|
||||
f'{{"entityUID": {x1.id}, "type": "constituenta", "cstType": "basic", "alias": "X1", "convention": "", '
|
||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}]}}'
|
||||
)
|
||||
self.assertEqual(schema.to_trs(), expected)
|
||||
def test_reset_aliases(self):
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
x1 = schema.insert_last('X11', CstType.BASE)
|
||||
x2 = schema.insert_last('X21', CstType.BASE)
|
||||
d1 = schema.insert_last('D11', CstType.TERM)
|
||||
x1.term_raw = 'человек'
|
||||
x1.term_resolved = 'человек'
|
||||
d1.convention = 'D11 - cool'
|
||||
d1.definition_formal = 'X21=X21'
|
||||
d1.term_raw = '@{X21|sing}'
|
||||
d1.definition_raw = '@{X11|datv}'
|
||||
d1.definition_resolved = 'test'
|
||||
d1.save()
|
||||
x1.save()
|
||||
|
||||
def test_create_from_trs(self):
|
||||
input = json.loads(
|
||||
'{"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)
|
||||
schema.reset_aliases()
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
self.assertEqual(schema.constituents().count(), 1)
|
||||
self.assertEqual(schema.title, input['title'])
|
||||
self.assertEqual(schema.alias, input['alias'])
|
||||
self.assertEqual(schema.comment, input['comment'])
|
||||
self.assertEqual(x2.alias, input['items'][0]['alias'])
|
||||
self.assertEqual(x2.convention, input['items'][0]['convention'])
|
||||
self.assertEqual(x2.term_raw, input['items'][0]['term']['raw'])
|
||||
self.assertEqual(x2.term_resolved, input['items'][0]['term']['raw'])
|
||||
self.assertEqual(x2.definition_formal, input['items'][0]['definition']['formal'])
|
||||
self.assertEqual(x2.definition_raw, input['items'][0]['definition']['text']['raw'])
|
||||
self.assertEqual(x2.definition_resolved, input['items'][0]['term']['raw'])
|
||||
d1.refresh_from_db()
|
||||
|
||||
self.assertEqual(x1.alias, 'X1')
|
||||
self.assertEqual(x2.alias, 'X2')
|
||||
self.assertEqual(d1.alias, 'D1')
|
||||
self.assertEqual(d1.convention, 'D1 - cool')
|
||||
self.assertEqual(d1.term_raw, '@{X2|sing}')
|
||||
self.assertEqual(d1.definition_raw, '@{X1|datv}')
|
||||
self.assertEqual(d1.definition_resolved, 'test')
|
||||
|
|
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 '''
|
||||
import json
|
||||
from io import BytesIO
|
||||
import re
|
||||
from zipfile import ZipFile
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
@ -35,13 +36,24 @@ def read_trs(file) -> dict:
|
|||
|
||||
def write_trs(json_data: dict) -> bytes:
|
||||
''' 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()
|
||||
data = json.dumps(json_data, indent=4, ensure_ascii=False)
|
||||
with ZipFile(content, 'w') as archive:
|
||||
archive.writestr('document.json', data=data)
|
||||
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):
|
||||
''' Endpoint: Get list of rsforms available for active user. '''
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.RSFormSerializer
|
||||
serializer_class = serializers.RSFormMetaSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
|
@ -45,7 +45,7 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView):
|
|||
class RSFormViewSet(viewsets.ModelViewSet):
|
||||
''' Endpoint: RSForm operations. '''
|
||||
queryset = models.RSForm.objects.all()
|
||||
serializer_class = serializers.RSFormSerializer
|
||||
serializer_class = serializers.RSFormMetaSerializer
|
||||
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filterset_fields = ['owner', 'is_common']
|
||||
|
@ -80,10 +80,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
data = serializer.validated_data
|
||||
new_cst = schema.create_cst(data, data['insert_after'] if 'insert_after' in data else None)
|
||||
schema.refresh_from_db()
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
response = Response(status=201, data={
|
||||
'new_cst': serializers.ConstituentaSerializer(new_cst).data,
|
||||
'schema': outSerializer.data
|
||||
'schema': models.PyConceptAdapter(schema).full()
|
||||
})
|
||||
response['Location'] = new_cst.get_absolute_url()
|
||||
return response
|
||||
|
@ -96,8 +95,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
schema.delete_cst(serializer.validated_data['constituents'])
|
||||
schema.refresh_from_db()
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=202, data=outSerializer.data)
|
||||
return Response(status=202, data=models.PyConceptAdapter(schema).full())
|
||||
|
||||
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
||||
def cst_moveto(self, request, pk):
|
||||
|
@ -107,17 +105,14 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
||||
schema.refresh_from_db()
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=200, data=outSerializer.data)
|
||||
return Response(status=200, data=models.PyConceptAdapter(schema).full())
|
||||
|
||||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||
def reset_aliases(self, request, pk):
|
||||
''' Endpoint: Recreate all aliases based on order. '''
|
||||
schema = self._get_schema()
|
||||
result = json.loads(pyconcept.reset_aliases(json.dumps(schema.to_trs())))
|
||||
schema.load_trs(data=result, sync_metadata=False, skip_update=True)
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=200, data=outSerializer.data)
|
||||
schema.reset_aliases()
|
||||
return Response(status=200, data=models.PyConceptAdapter(schema).full())
|
||||
|
||||
@action(detail=True, methods=['patch'], url_path='load-trs')
|
||||
def load_trs(self, request, pk):
|
||||
|
@ -127,25 +122,30 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
schema = self._get_schema()
|
||||
load_metadata = serializer.validated_data['load_metadata']
|
||||
data = utils.read_trs(request.FILES['file'].file)
|
||||
schema.load_trs(data, load_metadata, skip_update=False)
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(status=200, data=outSerializer.data)
|
||||
data['id'] = schema.pk
|
||||
|
||||
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')
|
||||
def clone(self, request, pk):
|
||||
''' 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)
|
||||
new_schema = models.RSForm.objects.create(
|
||||
title=serializer.validated_data['title'],
|
||||
owner=self.request.user,
|
||||
alias=serializer.validated_data.get('alias', ''),
|
||||
comment=serializer.validated_data.get('comment', ''),
|
||||
is_common=serializer.validated_data.get('is_common', False),
|
||||
)
|
||||
new_schema.load_trs(data=self._get_schema().to_trs(), sync_metadata=False, skip_update=True)
|
||||
outSerializer = serializers.RSFormDetailsSerlializer(new_schema)
|
||||
return Response(status=201, data=outSerializer.data)
|
||||
|
||||
clone_data = serializers.RSFormTRSSerializer(self._get_schema()).data
|
||||
clone_data['owner'] = self.request.user
|
||||
clone_data['title'] = serializer.validated_data['title']
|
||||
clone_data['alias'] = serializer.validated_data.get('alias', '')
|
||||
clone_data['comment'] = serializer.validated_data.get('comment', '')
|
||||
clone_data['is_common'] = serializer.validated_data.get('is_common', False)
|
||||
|
||||
clone = serializers.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
|
||||
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'])
|
||||
def claim(self, request, pk=None):
|
||||
|
@ -156,7 +156,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
else:
|
||||
schema.owner = self.request.user
|
||||
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'])
|
||||
def contents(self, request, pk):
|
||||
|
@ -166,25 +166,25 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
|
||||
@action(detail=True, methods=['get'])
|
||||
def details(self, request, pk):
|
||||
''' Endpoint: Detailed schema view including statuses. '''
|
||||
''' Endpoint: Detailed schema view including statuses and parse. '''
|
||||
schema = self._get_schema()
|
||||
serializer = serializers.RSFormDetailsSerlializer(schema)
|
||||
return Response(serializer.data)
|
||||
serializer = models.PyConceptAdapter(schema)
|
||||
return Response(serializer.full())
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def check(self, request, pk):
|
||||
''' 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.is_valid(raise_exception=True)
|
||||
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))
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||
def export_trs(self, request, pk):
|
||||
''' Endpoint: Download Exteor compatible file. '''
|
||||
schema = self._get_schema().to_trs()
|
||||
schema = serializers.RSFormTRSSerializer(self._get_schema()).data
|
||||
trs = utils.write_trs(schema)
|
||||
filename = self._get_schema().alias
|
||||
if filename == '' or not filename.isascii():
|
||||
|
@ -207,8 +207,11 @@ class TrsImportView(views.APIView):
|
|||
owner = self.request.user
|
||||
if owner.is_anonymous:
|
||||
owner = None
|
||||
schema = models.RSForm.create_from_trs(owner, data)
|
||||
result = serializers.RSFormSerializer(schema)
|
||||
_prepare_rsform_data(data, request, owner)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -219,7 +222,7 @@ def create_rsform(request):
|
|||
if owner.is_anonymous:
|
||||
owner = None
|
||||
if 'file' not in request.FILES:
|
||||
serializer = serializers.RSFormSerializer(data=request.data)
|
||||
serializer = serializers.RSFormMetaSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema = models.RSForm.objects.create(
|
||||
title=serializer.validated_data['title'],
|
||||
|
@ -230,6 +233,14 @@ def create_rsform(request):
|
|||
)
|
||||
else:
|
||||
data = utils.read_trs(request.FILES['file'].file)
|
||||
_prepare_rsform_data(data, request, owner)
|
||||
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)
|
||||
|
||||
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'] == '':
|
||||
|
@ -241,10 +252,8 @@ def create_rsform(request):
|
|||
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)
|
||||
|
||||
data['is_common'] = is_common
|
||||
data['owner'] = owner
|
||||
|
||||
@api_view(['POST'])
|
||||
def parse_expression(request):
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
''' Tests. '''
|
||||
# flake8: noqa
|
||||
from .t_views import *
|
||||
from .t_serializers import *
|
||||
|
|
|
@ -43,7 +43,7 @@ function App () {
|
|||
/>
|
||||
|
||||
<div className='overflow-auto' style={{maxHeight: scrollWindowSize}}>
|
||||
<main className='px-2' style={{minHeight: mainSize}}>
|
||||
<main style={{minHeight: mainSize}}>
|
||||
<Routes>
|
||||
<Route path='/' element={ <HomePage/>} />
|
||||
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
interface SubmitButtonProps {
|
||||
text: string
|
||||
interface SubmitButtonProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'> {
|
||||
text?: string
|
||||
tooltip?: string
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
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 (
|
||||
<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}
|
||||
>
|
||||
{icon && <span>{icon}</span>}
|
||||
|
|
|
@ -7,7 +7,7 @@ interface TextURLProps {
|
|||
|
||||
function TextURL({ text, href }: TextURLProps) {
|
||||
return (
|
||||
<Link className='font-bold hover:underline clr-text' to={href}>
|
||||
<Link className='font-bold hover:underline text-url' to={href}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -68,8 +68,9 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
|
|||
highlightOnHover
|
||||
pointerOnHover
|
||||
|
||||
noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>
|
||||
<p>Список схем пуст</p>
|
||||
noDataComponent={
|
||||
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[10rem]'>
|
||||
<p><b>Список схем пуст</b></p>
|
||||
<p>
|
||||
<TextURL text='Создать схему' href='/rsform-create'/>
|
||||
<span> | </span>
|
||||
|
@ -79,7 +80,7 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
|
|||
<b>Очистить фильтр</b>
|
||||
</span>
|
||||
</p>
|
||||
</span>}
|
||||
</div>}
|
||||
|
||||
pagination
|
||||
paginationPerPage={50}
|
||||
|
|
|
@ -39,7 +39,7 @@ function LoginPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='w-full py-2'> { user
|
||||
<div className='w-full py-4'> { user
|
||||
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
||||
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[21rem]'>
|
||||
<TextInput id='username'
|
||||
|
@ -58,11 +58,15 @@ function LoginPage() {
|
|||
onChange={event => { setPassword(event.target.value); }}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between mt-4'>
|
||||
<SubmitButton text='Вход' loading={loading}/>
|
||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||
<div className='flex justify-center w-full gap-2 mt-4'>
|
||||
<SubmitButton
|
||||
text='Вход'
|
||||
widthClass='w-[7rem]'
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<div className='flex flex-col mt-2 text-sm'>
|
||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
||||
</div>
|
||||
{ error && <BackendError error={error} />}
|
||||
|
|
|
@ -217,7 +217,7 @@ function RSTabs() {
|
|||
defaultFocus={true}
|
||||
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
|
||||
onDestroy={onDestroySchema}
|
||||
showCloneDialog={() => setShowClone(true)}
|
||||
|
@ -232,7 +232,7 @@ function RSTabs() {
|
|||
<ConceptTab>Граф термов</ConceptTab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel className='flex items-start w-full gap-2'>
|
||||
<TabPanel className='flex items-start w-full gap-2 px-2'>
|
||||
<EditorRSForm
|
||||
onDestroy={onDestroySchema}
|
||||
/>
|
||||
|
@ -247,7 +247,7 @@ function RSTabs() {
|
|||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<TabPanel className='pl-2'>
|
||||
<EditorConstituenta
|
||||
activeID={activeID}
|
||||
onOpenEdit={onOpenCst}
|
||||
|
@ -257,7 +257,7 @@ function RSTabs() {
|
|||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<TabPanel className='pl-2'>
|
||||
<EditorTermGraph
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
|
|
|
@ -21,7 +21,7 @@ function DependencyModePicker({ value, onChange }: DependencyModePickerProps) {
|
|||
}, [pickerMenu, onChange]);
|
||||
|
||||
return (
|
||||
<div ref={pickerMenu.ref}>
|
||||
<div ref={pickerMenu.ref} className='h-full'>
|
||||
<span
|
||||
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
||||
tabIndex={-1}
|
||||
|
|
|
@ -21,7 +21,7 @@ function MatchModePicker({ value, onChange }: MatchModePickerProps) {
|
|||
}, [pickerMenu, onChange]);
|
||||
|
||||
return (
|
||||
<div ref={pickerMenu.ref}>
|
||||
<div ref={pickerMenu.ref} className='h-full'>
|
||||
<span
|
||||
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
||||
tabIndex={-1}
|
||||
|
|
|
@ -152,13 +152,13 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
|||
}, [noNavigation, baseHeight]);
|
||||
|
||||
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
|
||||
value={filterMatch}
|
||||
onChange={setFilterMatch}
|
||||
/>
|
||||
<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='наберите текст фильтра'
|
||||
value={filterText}
|
||||
onChange={event => setFilterText(event.target.value)}
|
||||
|
|
Loading…
Reference in New Issue
Block a user