2023-08-17 15:43:39 +03:00
|
|
|
|
''' Models: RSForms for conceptual schemas. '''
|
2023-07-23 15:23:01 +03:00
|
|
|
|
import json
|
2023-08-17 15:43:39 +03:00
|
|
|
|
import pyconcept
|
2023-07-15 17:46:19 +03:00
|
|
|
|
from django.db import models, transaction
|
|
|
|
|
from django.core.validators import MinValueValidator
|
|
|
|
|
from django.core.exceptions import ValidationError
|
2023-07-24 22:34:03 +03:00
|
|
|
|
from django.urls import reverse
|
2023-07-15 17:46:19 +03:00
|
|
|
|
from apps.users.models import User
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CstType(models.TextChoices):
|
|
|
|
|
''' Type of constituenta '''
|
|
|
|
|
BASE = 'basic'
|
|
|
|
|
CONSTANT = 'constant'
|
|
|
|
|
STRUCTURED = 'structure'
|
|
|
|
|
AXIOM = 'axiom'
|
|
|
|
|
TERM = 'term'
|
|
|
|
|
FUNCTION = 'function'
|
|
|
|
|
PREDICATE = 'predicate'
|
|
|
|
|
THEOREM = 'theorem'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Syntax(models.TextChoices):
|
|
|
|
|
''' Syntax types '''
|
|
|
|
|
UNDEF = 'undefined'
|
|
|
|
|
ASCII = 'ascii'
|
|
|
|
|
MATH = 'math'
|
|
|
|
|
|
|
|
|
|
|
2023-07-24 22:34:03 +03:00
|
|
|
|
def _empty_forms():
|
|
|
|
|
return []
|
2023-07-15 17:46:19 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RSForm(models.Model):
|
|
|
|
|
''' RSForm is a math form of capturing conceptual schema '''
|
|
|
|
|
owner = models.ForeignKey(
|
|
|
|
|
verbose_name='Владелец',
|
|
|
|
|
to=User,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
null=True
|
|
|
|
|
)
|
|
|
|
|
title = models.TextField(
|
|
|
|
|
verbose_name='Название'
|
|
|
|
|
)
|
|
|
|
|
alias = models.CharField(
|
|
|
|
|
verbose_name='Шифр',
|
|
|
|
|
max_length=255,
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
comment = models.TextField(
|
|
|
|
|
verbose_name='Комментарий',
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
is_common = models.BooleanField(
|
|
|
|
|
verbose_name='Общая',
|
|
|
|
|
default=False
|
|
|
|
|
)
|
|
|
|
|
time_create = models.DateTimeField(
|
|
|
|
|
verbose_name='Дата создания',
|
|
|
|
|
auto_now_add=True
|
|
|
|
|
)
|
|
|
|
|
time_update = models.DateTimeField(
|
|
|
|
|
verbose_name='Дата изменения',
|
|
|
|
|
auto_now=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
2023-08-17 15:43:39 +03:00
|
|
|
|
''' Model metadata. '''
|
2023-07-15 17:46:19 +03:00
|
|
|
|
verbose_name = 'Схема'
|
|
|
|
|
verbose_name_plural = 'Схемы'
|
|
|
|
|
|
|
|
|
|
def constituents(self) -> models.QuerySet:
|
|
|
|
|
''' Get QuerySet containing all constituents of current RSForm '''
|
|
|
|
|
return Constituenta.objects.filter(schema=self)
|
|
|
|
|
|
|
|
|
|
@transaction.atomic
|
2023-08-17 15:43:39 +03:00
|
|
|
|
def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta':
|
2023-07-15 17:46:19 +03:00
|
|
|
|
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
|
|
|
|
|
if position <= 0:
|
|
|
|
|
raise ValidationError('Invalid position: should be positive integer')
|
2023-07-23 15:23:01 +03:00
|
|
|
|
update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self, order__gte=position)
|
2023-07-15 17:46:19 +03:00
|
|
|
|
for cst in update_list:
|
|
|
|
|
cst.order += 1
|
2023-07-23 15:23:01 +03:00
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
|
|
|
|
|
|
|
|
|
result = Constituenta.objects.create(
|
2023-07-15 17:46:19 +03:00
|
|
|
|
schema=self,
|
|
|
|
|
order=position,
|
|
|
|
|
alias=alias,
|
2023-08-17 15:43:39 +03:00
|
|
|
|
cst_type=insert_type
|
2023-07-15 17:46:19 +03:00
|
|
|
|
)
|
2023-07-24 22:34:03 +03:00
|
|
|
|
self._update_from_core()
|
2023-07-23 21:38:04 +03:00
|
|
|
|
self.save()
|
2023-07-26 23:11:00 +03:00
|
|
|
|
result.refresh_from_db()
|
|
|
|
|
return result
|
2023-07-15 17:46:19 +03:00
|
|
|
|
|
|
|
|
|
@transaction.atomic
|
2023-08-17 15:43:39 +03:00
|
|
|
|
def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta':
|
2023-07-15 17:46:19 +03:00
|
|
|
|
''' Insert new constituenta at last position '''
|
|
|
|
|
position = 1
|
|
|
|
|
if self.constituents().exists():
|
2023-07-27 22:04:25 +03:00
|
|
|
|
position += self.constituents().count()
|
2023-07-23 15:23:01 +03:00
|
|
|
|
result = Constituenta.objects.create(
|
2023-07-15 17:46:19 +03:00
|
|
|
|
schema=self,
|
|
|
|
|
order=position,
|
|
|
|
|
alias=alias,
|
2023-08-17 15:43:39 +03:00
|
|
|
|
cst_type=insert_type
|
2023-07-15 17:46:19 +03:00
|
|
|
|
)
|
2023-07-24 22:34:03 +03:00
|
|
|
|
self._update_from_core()
|
2023-07-23 21:38:04 +03:00
|
|
|
|
self.save()
|
2023-07-25 00:20:37 +03:00
|
|
|
|
result.refresh_from_db()
|
|
|
|
|
return result
|
2023-07-15 17:46:19 +03:00
|
|
|
|
|
2023-07-24 22:34:03 +03:00
|
|
|
|
@transaction.atomic
|
|
|
|
|
def move_cst(self, listCst: list['Constituenta'], target: int):
|
|
|
|
|
''' Move list of constituents to specific position '''
|
|
|
|
|
count_moved = 0
|
|
|
|
|
count_top = 0
|
|
|
|
|
count_bot = 0
|
|
|
|
|
size = len(listCst)
|
|
|
|
|
update_list = []
|
2023-07-27 22:04:25 +03:00
|
|
|
|
for cst in self.constituents().only('id', 'order').order_by('order'):
|
2023-07-24 22:34:03 +03:00
|
|
|
|
if cst not in listCst:
|
|
|
|
|
if count_top + 1 < target:
|
|
|
|
|
cst.order = count_top + 1
|
|
|
|
|
count_top += 1
|
|
|
|
|
else:
|
|
|
|
|
cst.order = target + size + count_bot
|
|
|
|
|
count_bot += 1
|
|
|
|
|
else:
|
|
|
|
|
cst.order = target + count_moved
|
|
|
|
|
count_moved += 1
|
|
|
|
|
update_list.append(cst)
|
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
|
|
|
|
self._update_from_core()
|
|
|
|
|
self.save()
|
|
|
|
|
|
2023-07-23 21:38:04 +03:00
|
|
|
|
@transaction.atomic
|
|
|
|
|
def delete_cst(self, listCst):
|
|
|
|
|
''' Delete multiple constituents. Do not check if listCst are from this schema '''
|
|
|
|
|
for cst in listCst:
|
|
|
|
|
cst.delete()
|
2023-07-24 22:34:03 +03:00
|
|
|
|
self._update_from_core()
|
2023-07-23 21:38:04 +03:00
|
|
|
|
self.save()
|
|
|
|
|
|
2023-07-27 22:04:25 +03:00
|
|
|
|
@transaction.atomic
|
|
|
|
|
def load_trs(self, data: dict, sync_metadata: bool, skip_update: bool):
|
|
|
|
|
if sync_metadata:
|
|
|
|
|
self.title = data.get('title', 'Без названия')
|
|
|
|
|
self.alias = data.get('alias', '')
|
|
|
|
|
self.comment = data.get('comment', '')
|
|
|
|
|
order = 1
|
|
|
|
|
prev_constituents = self.constituents()
|
|
|
|
|
loaded_ids = set()
|
|
|
|
|
for cst_data in data['items']:
|
|
|
|
|
uid = int(cst_data['entityUID'])
|
|
|
|
|
if prev_constituents.filter(pk=uid).exists():
|
|
|
|
|
cst: Constituenta = prev_constituents.get(pk=uid)
|
|
|
|
|
cst.order = order
|
|
|
|
|
cst.load_trs(cst_data)
|
|
|
|
|
cst.save()
|
|
|
|
|
else:
|
|
|
|
|
cst = Constituenta.create_from_trs(cst_data, self, order)
|
|
|
|
|
cst.save()
|
|
|
|
|
uid = cst.id
|
|
|
|
|
loaded_ids.add(uid)
|
|
|
|
|
order += 1
|
|
|
|
|
for prev_cst in prev_constituents:
|
|
|
|
|
if prev_cst.id not in loaded_ids:
|
|
|
|
|
prev_cst.delete()
|
|
|
|
|
if not skip_update:
|
|
|
|
|
self._update_from_core()
|
|
|
|
|
self.save()
|
|
|
|
|
|
2023-07-15 17:46:19 +03:00
|
|
|
|
@staticmethod
|
|
|
|
|
@transaction.atomic
|
2023-07-27 22:04:25 +03:00
|
|
|
|
def create_from_trs(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
2023-07-15 17:46:19 +03:00
|
|
|
|
schema = RSForm.objects.create(
|
|
|
|
|
title=data.get('title', 'Без названия'),
|
|
|
|
|
owner=owner,
|
|
|
|
|
alias=data.get('alias', ''),
|
|
|
|
|
comment=data.get('comment', ''),
|
|
|
|
|
is_common=is_common
|
|
|
|
|
)
|
2023-08-17 15:43:39 +03:00
|
|
|
|
# pylint: disable=protected-access
|
2023-07-27 22:04:25 +03:00
|
|
|
|
schema._create_items_from_trs(data['items'])
|
2023-07-15 17:46:19 +03:00
|
|
|
|
return schema
|
|
|
|
|
|
2023-07-27 22:04:25 +03:00
|
|
|
|
def to_trs(self) -> str:
|
2023-07-15 17:46:19 +03:00
|
|
|
|
''' Generate JSON string containing all data from RSForm '''
|
|
|
|
|
result = self._prepare_json_rsform()
|
2023-07-27 22:04:25 +03:00
|
|
|
|
items: list['Constituenta'] = self.constituents().order_by('order')
|
2023-07-15 17:46:19 +03:00
|
|
|
|
for cst in items:
|
2023-07-27 22:04:25 +03:00
|
|
|
|
result['items'].append(cst.to_trs())
|
2023-07-15 17:46:19 +03:00
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.title
|
|
|
|
|
|
2023-07-24 22:34:03 +03:00
|
|
|
|
def get_absolute_url(self):
|
|
|
|
|
return reverse('rsform-detail', kwargs={'pk': self.pk})
|
|
|
|
|
|
2023-07-15 17:46:19 +03:00
|
|
|
|
def _prepare_json_rsform(self: 'Constituenta') -> dict:
|
|
|
|
|
return {
|
|
|
|
|
'type': 'rsform',
|
|
|
|
|
'title': self.title,
|
|
|
|
|
'alias': self.alias,
|
|
|
|
|
'comment': self.comment,
|
|
|
|
|
'items': []
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-27 22:04:25 +03:00
|
|
|
|
@transaction.atomic
|
2023-07-24 22:34:03 +03:00
|
|
|
|
def _update_from_core(self) -> dict:
|
2023-07-27 22:04:25 +03:00
|
|
|
|
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
|
2023-07-23 15:23:01 +03:00
|
|
|
|
update_list = self.constituents().only('id', 'order')
|
2023-08-17 15:43:39 +03:00
|
|
|
|
if len(checked['items']) != update_list.count():
|
2023-07-23 15:23:01 +03:00
|
|
|
|
raise ValidationError
|
|
|
|
|
order = 1
|
|
|
|
|
for cst in checked['items']:
|
2023-08-17 15:43:39 +03:00
|
|
|
|
cst_id = cst['entityUID']
|
2023-07-23 15:23:01 +03:00
|
|
|
|
for oldCst in update_list:
|
2023-08-17 15:43:39 +03:00
|
|
|
|
if oldCst.id == cst_id:
|
2023-07-23 15:23:01 +03:00
|
|
|
|
oldCst.order = order
|
|
|
|
|
order += 1
|
|
|
|
|
break
|
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
2023-07-24 22:34:03 +03:00
|
|
|
|
return checked
|
2023-07-23 15:23:01 +03:00
|
|
|
|
|
2023-07-27 22:04:25 +03:00
|
|
|
|
@transaction.atomic
|
|
|
|
|
def _create_items_from_trs(self, items):
|
2023-07-23 15:23:01 +03:00
|
|
|
|
order = 1
|
|
|
|
|
for cst in items:
|
2023-08-17 15:43:39 +03:00
|
|
|
|
cst_object = Constituenta.create_from_trs(cst, self, order)
|
|
|
|
|
cst_object.save()
|
2023-07-23 15:23:01 +03:00
|
|
|
|
order += 1
|
2023-07-15 17:46:19 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Constituenta(models.Model):
|
|
|
|
|
''' Constituenta is the base unit for every conceptual schema '''
|
|
|
|
|
schema = models.ForeignKey(
|
|
|
|
|
verbose_name='Концептуальная схема',
|
|
|
|
|
to=RSForm,
|
|
|
|
|
on_delete=models.CASCADE
|
|
|
|
|
)
|
|
|
|
|
order = models.PositiveIntegerField(
|
|
|
|
|
verbose_name='Позиция',
|
2023-07-24 22:34:03 +03:00
|
|
|
|
validators=[MinValueValidator(1)],
|
|
|
|
|
default=-1,
|
2023-07-15 17:46:19 +03:00
|
|
|
|
)
|
|
|
|
|
alias = models.CharField(
|
|
|
|
|
verbose_name='Имя',
|
2023-07-24 22:34:03 +03:00
|
|
|
|
max_length=8,
|
|
|
|
|
default='undefined'
|
2023-07-15 17:46:19 +03:00
|
|
|
|
)
|
2023-07-26 23:11:00 +03:00
|
|
|
|
cst_type = models.CharField(
|
2023-07-15 17:46:19 +03:00
|
|
|
|
verbose_name='Тип',
|
|
|
|
|
max_length=10,
|
|
|
|
|
choices=CstType.choices,
|
|
|
|
|
default=CstType.BASE
|
|
|
|
|
)
|
|
|
|
|
convention = models.TextField(
|
|
|
|
|
verbose_name='Комментарий/Конвенция',
|
|
|
|
|
default='',
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
2023-07-24 22:34:03 +03:00
|
|
|
|
term_raw = models.TextField(
|
|
|
|
|
verbose_name='Термин (с отсылками)',
|
|
|
|
|
default='',
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
term_resolved = models.TextField(
|
2023-07-15 17:46:19 +03:00
|
|
|
|
verbose_name='Термин',
|
2023-07-24 22:34:03 +03:00
|
|
|
|
default='',
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
term_forms = models.JSONField(
|
|
|
|
|
verbose_name='Словоформы',
|
|
|
|
|
default=_empty_forms
|
2023-07-15 17:46:19 +03:00
|
|
|
|
)
|
|
|
|
|
definition_formal = models.TextField(
|
|
|
|
|
verbose_name='Родоструктурное определение',
|
|
|
|
|
default='',
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
2023-07-24 22:34:03 +03:00
|
|
|
|
definition_raw = models.TextField(
|
|
|
|
|
verbose_name='Текстовое определние (с отсылками)',
|
|
|
|
|
default='',
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
definition_resolved = models.TextField(
|
2023-07-15 17:46:19 +03:00
|
|
|
|
verbose_name='Текстовое определние',
|
2023-07-24 22:34:03 +03:00
|
|
|
|
default='',
|
2023-07-15 17:46:19 +03:00
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
2023-08-17 15:43:39 +03:00
|
|
|
|
''' Model metadata. '''
|
2023-07-15 17:46:19 +03:00
|
|
|
|
verbose_name = 'Конституета'
|
|
|
|
|
verbose_name_plural = 'Конституенты'
|
|
|
|
|
|
2023-07-24 22:34:03 +03:00
|
|
|
|
def get_absolute_url(self):
|
|
|
|
|
return reverse('constituenta-detail', kwargs={'pk': self.pk})
|
|
|
|
|
|
2023-07-15 17:46:19 +03:00
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.alias
|
2023-07-23 15:23:01 +03:00
|
|
|
|
|
2023-07-24 22:34:03 +03:00
|
|
|
|
@staticmethod
|
2023-07-27 22:04:25 +03:00
|
|
|
|
def create_from_trs(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
|
|
|
|
''' Create constituenta from TRS json '''
|
2023-07-24 22:34:03 +03:00
|
|
|
|
cst = Constituenta(
|
|
|
|
|
alias=data['alias'],
|
|
|
|
|
schema=schema,
|
|
|
|
|
order=order,
|
2023-07-26 23:11:00 +03:00
|
|
|
|
cst_type=data['cstType'],
|
2023-07-24 22:34:03 +03:00
|
|
|
|
)
|
2023-08-17 15:43:39 +03:00
|
|
|
|
# pylint: disable=protected-access
|
2023-07-27 22:04:25 +03:00
|
|
|
|
cst._load_texts(data)
|
|
|
|
|
return cst
|
|
|
|
|
|
|
|
|
|
def load_trs(self, data: dict):
|
|
|
|
|
''' Load data from TRS json '''
|
|
|
|
|
self.alias = data['alias']
|
|
|
|
|
self.cst_type = data['cstType']
|
|
|
|
|
self._load_texts(data)
|
|
|
|
|
|
|
|
|
|
def _load_texts(self, data: dict):
|
|
|
|
|
self.convention = data.get('convention', '')
|
2023-07-24 22:34:03 +03:00
|
|
|
|
if 'definition' in data:
|
2023-07-27 22:04:25 +03:00
|
|
|
|
self.definition_formal = data['definition'].get('formal', '')
|
2023-07-24 22:34:03 +03:00
|
|
|
|
if 'text' in data['definition']:
|
2023-07-27 22:04:25 +03:00
|
|
|
|
self.definition_raw = data['definition']['text'].get('raw', '')
|
|
|
|
|
self.definition_resolved = data['definition']['text'].get('resolved', '')
|
|
|
|
|
else:
|
|
|
|
|
self.definition_raw = ''
|
|
|
|
|
self.definition_resolved = ''
|
2023-07-24 22:34:03 +03:00
|
|
|
|
if 'term' in data:
|
2023-07-27 22:04:25 +03:00
|
|
|
|
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 = []
|
2023-07-24 22:34:03 +03:00
|
|
|
|
|
2023-07-27 22:04:25 +03:00
|
|
|
|
def to_trs(self) -> str:
|
2023-07-23 15:23:01 +03:00
|
|
|
|
return {
|
|
|
|
|
'entityUID': self.id,
|
|
|
|
|
'type': 'constituenta',
|
2023-07-26 23:11:00 +03:00
|
|
|
|
'cstType': self.cst_type,
|
2023-07-23 15:23:01 +03:00
|
|
|
|
'alias': self.alias,
|
|
|
|
|
'convention': self.convention,
|
2023-07-24 22:34:03 +03:00
|
|
|
|
'term': {
|
|
|
|
|
'raw': self.term_raw,
|
|
|
|
|
'resolved': self.term_resolved,
|
|
|
|
|
'forms': self.term_forms,
|
|
|
|
|
},
|
2023-07-23 15:23:01 +03:00
|
|
|
|
'definition': {
|
|
|
|
|
'formal': self.definition_formal,
|
2023-07-24 22:34:03 +03:00
|
|
|
|
'text': {
|
|
|
|
|
'raw': self.definition_raw,
|
|
|
|
|
'resolved': self.definition_resolved,
|
|
|
|
|
},
|
|
|
|
|
},
|
2023-07-23 15:23:01 +03:00
|
|
|
|
}
|