2024-03-11 18:08:28 +03:00
|
|
|
''' Models: RSForm API. '''
|
2025-08-01 10:55:53 +03:00
|
|
|
# pylint: disable=duplicate-code
|
2023-08-22 17:52:59 +03:00
|
|
|
|
2025-08-03 11:40:22 +03:00
|
|
|
from typing import Iterable, Optional, cast
|
2025-08-01 10:55:53 +03:00
|
|
|
|
|
|
|
from cctext import Entity, Resolver, TermForm, split_grams
|
2024-05-24 18:31:14 +03:00
|
|
|
from django.core.exceptions import ValidationError
|
2024-07-25 19:12:59 +03:00
|
|
|
from django.db.models import QuerySet
|
2023-08-22 17:52:59 +03:00
|
|
|
|
2024-07-25 19:12:59 +03:00
|
|
|
from apps.library.models import LibraryItem, LibraryItemType, Version
|
2024-07-19 19:29:27 +03:00
|
|
|
from shared import messages as msg
|
|
|
|
|
2025-08-03 11:40:22 +03:00
|
|
|
from ..graph import Graph
|
|
|
|
from .api_RSLanguage import get_type_prefix, guess_type
|
|
|
|
from .Constituenta import Constituenta, CstType, extract_entities, extract_globals
|
2024-03-11 18:08:28 +03:00
|
|
|
|
2024-08-09 20:58:28 +03:00
|
|
|
INSERT_LAST: int = -1
|
2024-08-11 12:38:08 +03:00
|
|
|
DELETED_ALIAS = 'DEL'
|
2024-03-22 17:01:14 +03:00
|
|
|
|
|
|
|
|
2024-07-25 19:12:59 +03:00
|
|
|
class RSForm:
|
2025-08-01 10:55:53 +03:00
|
|
|
''' RSForm wrapper. No caching, each mutation requires querying. '''
|
2024-05-24 19:06:39 +03:00
|
|
|
|
2024-07-25 19:12:59 +03:00
|
|
|
def __init__(self, model: LibraryItem):
|
2025-08-01 10:55:53 +03:00
|
|
|
assert model.item_type == LibraryItemType.RSFORM
|
2024-07-25 19:12:59 +03:00
|
|
|
self.model = model
|
2023-08-25 22:51:20 +03:00
|
|
|
|
2024-07-25 19:12:59 +03:00
|
|
|
@staticmethod
|
|
|
|
def create(**kwargs) -> 'RSForm':
|
|
|
|
''' Create LibraryItem via RSForm. '''
|
|
|
|
model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)
|
|
|
|
return RSForm(model)
|
2024-07-22 21:20:51 +03:00
|
|
|
|
2024-07-25 19:12:59 +03:00
|
|
|
@staticmethod
|
2025-08-03 11:40:22 +03:00
|
|
|
def resolver_from_schema(schemaID: int) -> Resolver:
|
2023-08-21 00:25:12 +03:00
|
|
|
''' Create resolver for text references based on schema terms. '''
|
|
|
|
result = Resolver({})
|
2025-08-01 10:55:53 +03:00
|
|
|
constituents = Constituenta.objects.filter(schema_id=schemaID).only('alias', 'term_resolved', 'term_forms')
|
|
|
|
for cst in constituents:
|
2023-09-25 14:17:52 +03:00
|
|
|
entity = Entity(
|
|
|
|
alias=cst.alias,
|
|
|
|
nominal=cst.term_resolved,
|
|
|
|
manual_forms=[
|
|
|
|
TermForm(text=form['text'], grams=split_grams(form['tags']))
|
|
|
|
for form in cst.term_forms
|
|
|
|
]
|
|
|
|
)
|
2023-08-21 00:25:12 +03:00
|
|
|
result.context[cst.alias] = entity
|
|
|
|
return result
|
|
|
|
|
2025-08-03 11:40:22 +03:00
|
|
|
@staticmethod
|
|
|
|
def resolver_from_list(cst_list: Iterable[Constituenta]) -> Resolver:
|
|
|
|
''' Create resolver for text references based on list of constituents. '''
|
|
|
|
result = Resolver({})
|
|
|
|
for cst in cst_list:
|
|
|
|
entity = Entity(
|
|
|
|
alias=cst.alias,
|
|
|
|
nominal=cst.term_resolved,
|
|
|
|
manual_forms=[
|
|
|
|
TermForm(text=form['text'], grams=split_grams(form['tags']))
|
|
|
|
for form in cst.term_forms
|
|
|
|
]
|
|
|
|
)
|
|
|
|
result.context[cst.alias] = entity
|
|
|
|
return result
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def graph_formal(cst_list: Iterable[Constituenta],
|
|
|
|
cst_by_alias: Optional[dict[str, Constituenta]] = None) -> Graph[int]:
|
|
|
|
''' Graph based on formal definitions. '''
|
|
|
|
result: Graph[int] = Graph()
|
|
|
|
if cst_by_alias is None:
|
|
|
|
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
|
|
|
for cst in cst_list:
|
|
|
|
result.add_node(cst.pk)
|
|
|
|
for cst in cst_list:
|
|
|
|
for alias in extract_globals(cst.definition_formal):
|
|
|
|
child = cst_by_alias.get(alias)
|
|
|
|
if child is not None:
|
|
|
|
result.add_edge(src=child.pk, dest=cst.pk)
|
|
|
|
return result
|
2024-07-15 12:36:18 +03:00
|
|
|
|
2025-08-03 11:40:22 +03:00
|
|
|
@staticmethod
|
|
|
|
def graph_term(cst_list: Iterable[Constituenta],
|
|
|
|
cst_by_alias: Optional[dict[str, Constituenta]] = None) -> Graph[int]:
|
|
|
|
''' Graph based on term texts. '''
|
|
|
|
result: Graph[int] = Graph()
|
|
|
|
if cst_by_alias is None:
|
|
|
|
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
|
|
|
for cst in cst_list:
|
|
|
|
result.add_node(cst.pk)
|
|
|
|
for cst in cst_list:
|
|
|
|
for alias in extract_entities(cst.term_raw):
|
|
|
|
child = cst_by_alias.get(alias)
|
|
|
|
if child is not None:
|
|
|
|
result.add_edge(src=child.pk, dest=cst.pk)
|
|
|
|
return result
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def graph_text(cst_list: Iterable[Constituenta],
|
|
|
|
cst_by_alias: Optional[Optional[dict[str, Constituenta]]] = None) -> Graph[int]:
|
|
|
|
''' Graph based on definition texts. '''
|
|
|
|
result: Graph[int] = Graph()
|
|
|
|
if cst_by_alias is None:
|
|
|
|
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
|
|
|
for cst in cst_list:
|
|
|
|
result.add_node(cst.pk)
|
|
|
|
for cst in cst_list:
|
|
|
|
for alias in extract_entities(cst.definition_raw):
|
|
|
|
child = cst_by_alias.get(alias)
|
|
|
|
if child is not None:
|
|
|
|
result.add_edge(src=child.pk, dest=cst.pk)
|
|
|
|
return result
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def save_order(cst_list: Iterable[Constituenta]) -> None:
|
|
|
|
''' Save order for constituents list. '''
|
|
|
|
order = 0
|
|
|
|
changed: list[Constituenta] = []
|
|
|
|
for cst in cst_list:
|
|
|
|
if cst.order != order:
|
|
|
|
cst.order = order
|
|
|
|
changed.append(cst)
|
|
|
|
order += 1
|
|
|
|
Constituenta.objects.bulk_update(changed, ['order'])
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def shift_positions(start: int, shift: int, cst_list: list[Constituenta]) -> None:
|
|
|
|
''' Shift positions of constituents. '''
|
|
|
|
if shift == 0:
|
|
|
|
return
|
|
|
|
update_list = cst_list[start:]
|
|
|
|
for cst in update_list:
|
|
|
|
cst.order += shift
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def apply_mapping(mapping: dict[str, str], cst_list: Iterable[Constituenta],
|
|
|
|
change_aliases: bool = False) -> None:
|
|
|
|
''' Apply rename mapping. '''
|
|
|
|
update_list: list[Constituenta] = []
|
|
|
|
for cst in cst_list:
|
|
|
|
if cst.apply_mapping(mapping, change_aliases):
|
|
|
|
update_list.append(cst)
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def resolve_term_change(cst_list: Iterable[Constituenta], changed: list[int],
|
|
|
|
cst_by_alias: Optional[Optional[dict[str, Constituenta]]] = None,
|
|
|
|
cst_by_id: Optional[Optional[dict[int, Constituenta]]] = None,
|
|
|
|
resolver: Optional[Resolver] = None) -> None:
|
|
|
|
''' Trigger cascade resolutions when term changes. '''
|
|
|
|
if cst_by_alias is None:
|
|
|
|
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
|
|
|
if cst_by_id is None:
|
|
|
|
cst_by_id = {cst.pk: cst for cst in cst_list}
|
|
|
|
|
|
|
|
graph_terms = RSForm.graph_term(cst_list, cst_by_alias)
|
|
|
|
expansion = graph_terms.expand_outputs(changed)
|
|
|
|
expanded_change = changed + expansion
|
|
|
|
update_list: list[Constituenta] = []
|
|
|
|
|
|
|
|
if resolver is None:
|
|
|
|
resolver = RSForm.resolver_from_list(cst_list)
|
|
|
|
|
|
|
|
if len(expansion) > 0:
|
|
|
|
for cst_id in graph_terms.topological_order():
|
|
|
|
if cst_id not in expansion:
|
|
|
|
continue
|
|
|
|
cst = cst_by_id[cst_id]
|
|
|
|
resolved = resolver.resolve(cst.term_raw)
|
|
|
|
if resolved == resolver.context[cst.alias].get_nominal():
|
|
|
|
continue
|
|
|
|
cst.set_term_resolved(resolved)
|
|
|
|
update_list.append(cst)
|
|
|
|
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
|
|
|
|
|
|
|
graph_defs = RSForm.graph_text(cst_list, cst_by_alias)
|
|
|
|
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
|
|
|
|
update_list = []
|
|
|
|
if len(update_defs) == 0:
|
|
|
|
return
|
|
|
|
for cst_id in update_defs:
|
|
|
|
cst = cst_by_id[cst_id]
|
|
|
|
resolved = resolver.resolve(cst.definition_raw)
|
|
|
|
cst.definition_resolved = resolved
|
|
|
|
update_list.append(cst)
|
|
|
|
Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
|
2024-07-15 12:36:18 +03:00
|
|
|
|
2025-08-01 10:55:53 +03:00
|
|
|
def constituentsQ(self) -> QuerySet[Constituenta]:
|
|
|
|
''' Get QuerySet containing all constituents of current RSForm. '''
|
|
|
|
return Constituenta.objects.filter(schema=self.model)
|
2024-07-15 12:36:18 +03:00
|
|
|
|
2025-08-01 10:55:53 +03:00
|
|
|
def insert_last(
|
2024-03-23 20:53:23 +03:00
|
|
|
self,
|
|
|
|
alias: str,
|
2024-07-15 12:36:18 +03:00
|
|
|
cst_type: Optional[CstType] = None,
|
2024-03-23 20:53:23 +03:00
|
|
|
**kwargs
|
|
|
|
) -> Constituenta:
|
2025-08-01 10:55:53 +03:00
|
|
|
''' Insert new constituenta at last position. '''
|
|
|
|
if Constituenta.objects.filter(schema=self.model, alias=alias).exists():
|
2024-03-22 17:01:14 +03:00
|
|
|
raise ValidationError(msg.aliasTaken(alias))
|
2024-03-23 20:53:23 +03:00
|
|
|
if cst_type is None:
|
|
|
|
cst_type = guess_type(alias)
|
2025-08-03 11:40:22 +03:00
|
|
|
position = Constituenta.objects.filter(schema=self.model).count()
|
2023-07-23 15:23:01 +03:00
|
|
|
result = Constituenta.objects.create(
|
2024-07-25 19:12:59 +03:00
|
|
|
schema=self.model,
|
2023-07-15 17:46:19 +03:00
|
|
|
order=position,
|
|
|
|
alias=alias,
|
2024-03-23 20:53:23 +03:00
|
|
|
cst_type=cst_type,
|
|
|
|
**kwargs
|
2023-07-15 17:46:19 +03:00
|
|
|
)
|
2023-07-26 23:11:00 +03:00
|
|
|
return result
|
2023-07-15 17:46:19 +03:00
|
|
|
|
2024-08-07 21:54:50 +03:00
|
|
|
def move_cst(self, target: list[Constituenta], destination: int) -> None:
|
2025-08-01 10:55:53 +03:00
|
|
|
''' Move list of constituents to specific position. '''
|
2023-07-24 22:34:03 +03:00
|
|
|
count_moved = 0
|
|
|
|
count_top = 0
|
|
|
|
count_bot = 0
|
2024-08-07 21:54:50 +03:00
|
|
|
size = len(target)
|
|
|
|
|
2025-08-01 10:55:53 +03:00
|
|
|
cst_list = Constituenta.objects.filter(schema=self.model).only('order').order_by('order')
|
2024-08-07 21:54:50 +03:00
|
|
|
for cst in cst_list:
|
|
|
|
if cst in target:
|
|
|
|
cst.order = destination + count_moved
|
2023-07-24 22:34:03 +03:00
|
|
|
count_moved += 1
|
2024-09-11 20:06:58 +03:00
|
|
|
elif count_top < destination:
|
|
|
|
cst.order = count_top
|
2024-08-07 21:54:50 +03:00
|
|
|
count_top += 1
|
|
|
|
else:
|
|
|
|
cst.order = destination + size + count_bot
|
|
|
|
count_bot += 1
|
|
|
|
Constituenta.objects.bulk_update(cst_list, ['order'])
|
2023-07-24 22:34:03 +03:00
|
|
|
|
2025-08-03 11:40:22 +03:00
|
|
|
def reset_aliases(self) -> None:
|
|
|
|
''' Recreate all aliases based on constituents order. '''
|
|
|
|
bases = cast(dict[str, int], {})
|
|
|
|
mapping = cast(dict[str, str], {})
|
|
|
|
for cst_type in CstType.values:
|
|
|
|
bases[cst_type] = 1
|
|
|
|
cst_list = Constituenta.objects.filter(schema=self.model).only(
|
|
|
|
'alias', 'cst_type', 'definition_formal',
|
|
|
|
'term_raw', 'definition_raw'
|
|
|
|
).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
|
|
|
|
RSForm.apply_mapping(mapping, cst_list, change_aliases=True)
|
2023-07-23 21:38:04 +03:00
|
|
|
|
2025-08-03 11:40:22 +03:00
|
|
|
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
|
|
|
''' Execute constituenta substitution. '''
|
|
|
|
if len(substitutions) < 1:
|
|
|
|
return
|
|
|
|
mapping = {}
|
|
|
|
deleted: list[int] = []
|
|
|
|
replacements: list[int] = []
|
|
|
|
for original, substitution in substitutions:
|
|
|
|
mapping[original.alias] = substitution.alias
|
|
|
|
deleted.append(original.pk)
|
|
|
|
replacements.append(substitution.pk)
|
|
|
|
Constituenta.objects.filter(pk__in=deleted).delete()
|
|
|
|
cst_list = Constituenta.objects.filter(schema=self.model).only(
|
|
|
|
'alias', 'cst_type', 'definition_formal',
|
|
|
|
'term_raw', 'definition_raw', 'order', 'term_forms', 'term_resolved'
|
|
|
|
).order_by('order')
|
|
|
|
RSForm.save_order(cst_list)
|
|
|
|
RSForm.apply_mapping(mapping, cst_list, change_aliases=False)
|
|
|
|
RSForm.resolve_term_change(cst_list, replacements)
|
2023-07-15 17:46:19 +03:00
|
|
|
|
2024-03-03 22:00:22 +03:00
|
|
|
def create_version(self, version: str, description: str, data) -> Version:
|
|
|
|
''' Creates version for current state. '''
|
|
|
|
return Version.objects.create(
|
2024-07-25 19:12:59 +03:00
|
|
|
item=self.model,
|
2024-03-03 22:00:22 +03:00
|
|
|
version=version,
|
|
|
|
description=description,
|
|
|
|
data=data
|
|
|
|
)
|