Change data model and refactor backend

This commit is contained in:
IRBorisov 2023-08-25 22:51:20 +03:00
parent 5d45094640
commit fba16a3d0b
24 changed files with 11336 additions and 612 deletions

View File

@ -5,6 +5,7 @@ Param(
$pyExec = "$PSScriptRoot\backend\venv\Scripts\python.exe" $pyExec = "$PSScriptRoot\backend\venv\Scripts\python.exe"
$djangoSrc = "$PSScriptRoot\backend\manage.py" $djangoSrc = "$PSScriptRoot\backend\manage.py"
$initialData = "fixtures/InitialData.json"
function RunServer() { function RunServer() {
RunBackend RunBackend
@ -20,6 +21,7 @@ function RunBackend() {
FlushData FlushData
DoMigrations DoMigrations
PrepareStatic -clearPrevious PrepareStatic -clearPrevious
AddInitialData
AddAdmin AddAdmin
} else { } else {
DoMigrations DoMigrations
@ -35,13 +37,16 @@ function RunFrontend() {
} }
function FlushData { function FlushData {
& $pyExec $djangoSrc flush --no-input\ & $pyExec $djangoSrc flush --noinput
$dbPath = "$PSScriptRoot\backend\db.sqlite3" $dbPath = "$PSScriptRoot\backend\db.sqlite3"
if (Test-Path -Path $dbPath -PathType Leaf) { if (Test-Path -Path $dbPath -PathType Leaf) {
Remove-Item $dbPath Remove-Item $dbPath
} }
} }
function AddInitialData {
& $pyExec manage.py loaddata $initialData
}
function AddAdmin { function AddAdmin {
$env:DJANGO_SUPERUSER_USERNAME = 'admin' $env:DJANGO_SUPERUSER_USERNAME = 'admin'
$env:DJANGO_SUPERUSER_PASSWORD = '1234' $env:DJANGO_SUPERUSER_PASSWORD = '1234'

View File

@ -8,9 +8,9 @@ class ConstituentaAdmin(admin.ModelAdmin):
''' Admin model: Constituenta. ''' ''' Admin model: Constituenta. '''
class RSFormAdmin(admin.ModelAdmin): class Librarydmin(admin.ModelAdmin):
''' Admin model: RSForm. ''' ''' Admin model: LibraryItem. '''
admin.site.register(models.Constituenta, ConstituentaAdmin) admin.site.register(models.Constituenta, ConstituentaAdmin)
admin.site.register(models.RSForm, RSFormAdmin) admin.site.register(models.LibraryItem, Librarydmin)

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.3 on 2023-07-26 15:19 # Generated by Django 4.2.4 on 2023-08-25 12:15
import apps.rsform.models import apps.rsform.models
from django.conf import settings from django.conf import settings
@ -17,13 +17,15 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='RSForm', name='LibraryItem',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item_type', models.CharField(choices=[('rsform', 'Rsform'), ('oss', 'Operations Schema')], max_length=50, verbose_name='Тип')),
('title', models.TextField(verbose_name='Название')), ('title', models.TextField(verbose_name='Название')),
('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')), ('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')),
('comment', models.TextField(blank=True, verbose_name='Комментарий')), ('comment', models.TextField(blank=True, verbose_name='Комментарий')),
('is_common', models.BooleanField(default=False, verbose_name='Общая')), ('is_common', models.BooleanField(default=False, verbose_name='Общая')),
('is_canonical', models.BooleanField(default=False, verbose_name='Каноничная')),
('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('time_create', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')), ('time_update', models.DateTimeField(auto_now=True, verbose_name='Дата изменения')),
('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец')), ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец')),
@ -33,6 +35,18 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Схемы', 'verbose_name_plural': 'Схемы',
}, },
), ),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
],
options={
'verbose_name': 'Подписки',
'verbose_name_plural': 'Подписка',
},
),
migrations.CreateModel( migrations.CreateModel(
name='Constituenta', name='Constituenta',
fields=[ fields=[
@ -47,7 +61,7 @@ class Migration(migrations.Migration):
('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')), ('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')),
('definition_raw', models.TextField(blank=True, default='', verbose_name='Текстовое определние (с отсылками)')), ('definition_raw', models.TextField(blank=True, default='', verbose_name='Текстовое определние (с отсылками)')),
('definition_resolved', models.TextField(blank=True, default='', verbose_name='Текстовое определние')), ('definition_resolved', models.TextField(blank=True, default='', verbose_name='Текстовое определние')),
('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема')), ('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Концептуальная схема')),
], ],
options={ options={
'verbose_name': 'Конституета', 'verbose_name': 'Конституета',

View File

@ -24,6 +24,12 @@ _REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') _GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)')
class LibraryItemType(TextChoices):
''' Type of library items '''
RSFORM = 'rsform'
OPERATIONS_SCHEMA = 'oss'
class CstType(TextChoices): class CstType(TextChoices):
''' Type of constituenta ''' ''' Type of constituenta '''
BASE = 'basic' BASE = 'basic'
@ -67,8 +73,14 @@ def _get_type_prefix(cst_type: CstType) -> str:
return 'X' return 'X'
class RSForm(Model): class LibraryItem(Model):
''' RSForm is a math form of capturing conceptual schema ''' ''' Abstract library item.
Please use wrappers below to access functionality. '''
item_type: CharField = CharField(
verbose_name='Тип',
max_length=50,
choices=LibraryItemType.choices
)
owner: ForeignKey = ForeignKey( owner: ForeignKey = ForeignKey(
verbose_name='Владелец', verbose_name='Владелец',
to=User, to=User,
@ -91,6 +103,10 @@ class RSForm(Model):
verbose_name='Общая', verbose_name='Общая',
default=False default=False
) )
is_canonical: BooleanField = BooleanField(
verbose_name='Каноничная',
default=False
)
time_create: DateTimeField = DateTimeField( time_create: DateTimeField = DateTimeField(
verbose_name='Дата создания', verbose_name='Дата создания',
auto_now_add=True auto_now_add=True
@ -105,9 +121,127 @@ class RSForm(Model):
verbose_name = 'Схема' verbose_name = 'Схема'
verbose_name_plural = 'Схемы' verbose_name_plural = 'Схемы'
def __str__(self) -> str:
return f'{self.title}'
def get_absolute_url(self):
return f'/api/library/{self.pk}/'
class Subscription(Model):
''' User subscription to library item. '''
user: ForeignKey = ForeignKey(
verbose_name='Пользователь',
to=User,
on_delete=CASCADE
)
item: ForeignKey = ForeignKey(
verbose_name='Элемент',
to=LibraryItem,
on_delete=CASCADE
)
class Meta:
''' Model metadata. '''
verbose_name = 'Подписки'
verbose_name_plural = 'Подписка'
def __str__(self) -> str:
return f'{self.user} -> {self.item}'
class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema '''
schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема',
to=LibraryItem,
on_delete=CASCADE
)
order: PositiveIntegerField = PositiveIntegerField(
verbose_name='Позиция',
validators=[MinValueValidator(1)],
default=-1,
)
alias: CharField = CharField(
verbose_name='Имя',
max_length=8,
default='undefined'
)
cst_type: CharField = CharField(
verbose_name='Тип',
max_length=10,
choices=CstType.choices,
default=CstType.BASE
)
convention: TextField = TextField(
verbose_name='Комментарий/Конвенция',
default='',
blank=True
)
term_raw: TextField = TextField(
verbose_name='Термин (с отсылками)',
default='',
blank=True
)
term_resolved: TextField = TextField(
verbose_name='Термин',
default='',
blank=True
)
term_forms: JSONField = JSONField(
verbose_name='Словоформы',
default=_empty_forms
)
definition_formal: TextField = TextField(
verbose_name='Родоструктурное определение',
default='',
blank=True
)
definition_raw: TextField = TextField(
verbose_name='Текстовое определние (с отсылками)',
default='',
blank=True
)
definition_resolved: TextField = TextField(
verbose_name='Текстовое определние',
default='',
blank=True
)
class Meta:
''' Model metadata. '''
verbose_name = 'Конституета'
verbose_name_plural = 'Конституенты'
def get_absolute_url(self):
''' URL access. '''
return reverse('constituenta-detail', kwargs={'pk': self.pk})
def __str__(self) -> str:
return f'{self.alias}'
def set_term_resolved(self, new_term: str):
''' Set term and reset forms if needed. '''
if new_term == self.term_resolved:
return
self.term_resolved = new_term
self.term_forms = []
class RSForm:
''' RSForm is a math form of capturing conceptual schema. '''
def __init__(self, item: LibraryItem):
if item.item_type != LibraryItemType.RSFORM:
raise ValueError('Attempting to use invalid adaptor for non-RSForm item')
self.item = item
@staticmethod
def create(**kwargs) -> 'RSForm':
return RSForm(LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs))
def constituents(self) -> QuerySet['Constituenta']: def constituents(self) -> QuerySet['Constituenta']:
''' Get QuerySet containing all constituents of current RSForm ''' ''' Get QuerySet containing all constituents of current RSForm. '''
return Constituenta.objects.filter(schema=self) return Constituenta.objects.filter(schema=self.item)
def resolver(self) -> Resolver: def resolver(self) -> Resolver:
''' Create resolver for text references based on schema terms. ''' ''' Create resolver for text references based on schema terms. '''
@ -152,19 +286,19 @@ class RSForm(Model):
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position ''' ''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
if position <= 0: if position <= 0:
raise ValidationError('Invalid position: should be positive integer') raise ValidationError('Invalid position: should be positive integer')
update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self, order__gte=position) update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self.item, order__gte=position)
for cst in update_list: for cst in update_list:
cst.order += 1 cst.order += 1
Constituenta.objects.bulk_update(update_list, ['order']) Constituenta.objects.bulk_update(update_list, ['order'])
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self, schema=self.item,
order=position, order=position,
alias=alias, alias=alias,
cst_type=insert_type cst_type=insert_type
) )
self.update_order() self.update_order()
self.save() self.item.save()
result.refresh_from_db() result.refresh_from_db()
return result return result
@ -175,13 +309,13 @@ class RSForm(Model):
if self.constituents().exists(): if self.constituents().exists():
position += self.constituents().count() position += self.constituents().count()
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self, schema=self.item,
order=position, order=position,
alias=alias, alias=alias,
cst_type=insert_type cst_type=insert_type
) )
self.update_order() self.update_order()
self.save() self.item.save()
result.refresh_from_db() result.refresh_from_db()
return result return result
@ -207,7 +341,7 @@ class RSForm(Model):
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.item.save()
@transaction.atomic @transaction.atomic
def delete_cst(self, listCst): def delete_cst(self, listCst):
@ -216,7 +350,7 @@ class RSForm(Model):
cst.delete() cst.delete()
self.update_order() self.update_order()
self.resolve_all_text() self.resolve_all_text()
self.save() self.item.save()
@transaction.atomic @transaction.atomic
def create_cst(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta': def create_cst(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta':
@ -326,12 +460,6 @@ class RSForm(Model):
else: else:
return self.insert_last(data['alias'], data['cst_type']) 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')
@ -355,83 +483,6 @@ class RSForm(Model):
return result return result
class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema '''
schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема',
to=RSForm,
on_delete=CASCADE
)
order: PositiveIntegerField = PositiveIntegerField(
verbose_name='Позиция',
validators=[MinValueValidator(1)],
default=-1,
)
alias: CharField = CharField(
verbose_name='Имя',
max_length=8,
default='undefined'
)
cst_type: CharField = CharField(
verbose_name='Тип',
max_length=10,
choices=CstType.choices,
default=CstType.BASE
)
convention: TextField = TextField(
verbose_name='Комментарий/Конвенция',
default='',
blank=True
)
term_raw: TextField = TextField(
verbose_name='Термин (с отсылками)',
default='',
blank=True
)
term_resolved: TextField = TextField(
verbose_name='Термин',
default='',
blank=True
)
term_forms: JSONField = JSONField(
verbose_name='Словоформы',
default=_empty_forms
)
definition_formal: TextField = TextField(
verbose_name='Родоструктурное определение',
default='',
blank=True
)
definition_raw: TextField = TextField(
verbose_name='Текстовое определние (с отсылками)',
default='',
blank=True
)
definition_resolved: TextField = TextField(
verbose_name='Текстовое определние',
default='',
blank=True
)
class Meta:
''' Model metadata. '''
verbose_name = 'Конституета'
verbose_name_plural = 'Конституенты'
def get_absolute_url(self):
''' URL access. '''
return reverse('constituenta-detail', kwargs={'pk': self.pk})
def __str__(self) -> str:
return f'{self.alias}'
def set_term_resolved(self, new_term: str):
''' Set term and reset forms if needed. '''
if new_term == self.term_resolved:
return
self.term_resolved = new_term
self.term_forms = []
class PyConceptAdapter: class PyConceptAdapter:
''' RSForm adapter for interacting with pyconcept module. ''' ''' RSForm adapter for interacting with pyconcept module. '''
def __init__(self, instance: RSForm): def __init__(self, instance: RSForm):
@ -456,14 +507,14 @@ class PyConceptAdapter:
def _complete_rsform_details(self, data: dict) -> dict: def _complete_rsform_details(self, data: dict) -> dict:
result = deepcopy(data) result = deepcopy(data)
result['id'] = self.schema.pk result['id'] = self.schema.item.pk
result['alias'] = self.schema.alias result['alias'] = self.schema.item.alias
result['title'] = self.schema.title result['title'] = self.schema.item.title
result['comment'] = self.schema.comment result['comment'] = self.schema.item.comment
result['time_update'] = self.schema.time_update result['time_update'] = self.schema.item.time_update
result['time_create'] = self.schema.time_create result['time_create'] = self.schema.item.time_create
result['is_common'] = self.schema.is_common result['is_common'] = self.schema.item.is_common
result['owner'] = (self.schema.owner.pk if self.schema.owner is not None else None) result['owner'] = (self.schema.item.owner.pk if self.schema.item.owner is not None else None)
for cst_data in result['items']: for cst_data in result['items']:
cst = Constituenta.objects.get(pk=cst_data['id']) cst = Constituenta.objects.get(pk=cst_data['id'])
cst_data['convention'] = cst.convention cst_data['convention'] = cst.convention

View File

@ -6,7 +6,7 @@ from django.db import transaction
from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference
from .utils import fix_old_references from .utils import fix_old_references
from .models import Constituenta, RSForm from .models import Constituenta, LibraryItem, RSForm
_CST_TYPE = 'constituenta' _CST_TYPE = 'constituenta'
_TRS_TYPE = 'rsform' _TRS_TYPE = 'rsform'
@ -30,13 +30,27 @@ class TextSerializer(serializers.Serializer):
text = serializers.CharField() text = serializers.CharField()
class RSFormMetaSerializer(serializers.ModelSerializer): class LibraryItemSerializer(serializers.ModelSerializer):
''' Serializer: General purpose RSForm data. ''' ''' Serializer: Library item data. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = '__all__'
read_only_fields = ('owner', 'id', 'item_type')
class RSFormSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for RSForm. '''
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = RSForm model = RSForm
fields = '__all__'
read_only_fields = ('owner', 'id') def to_representation(self, instance: RSForm):
result = LibraryItemSerializer(instance.item).data
result['items'] = []
for cst in instance.constituents().order_by('order'):
result['items'].append(ConstituentaSerializer(cst).data)
return result
class RSFormUploadSerializer(serializers.Serializer): class RSFormUploadSerializer(serializers.Serializer):
@ -45,26 +59,8 @@ class RSFormUploadSerializer(serializers.Serializer):
load_metadata = serializers.BooleanField() load_metadata = serializers.BooleanField()
class RSFormContentsSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for RSForm. '''
class Meta:
''' serializer metadata. '''
model = RSForm
def to_representation(self, instance: RSForm):
result = RSFormMetaSerializer(instance).data
result['items'] = []
for cst in instance.constituents().order_by('order'):
result['items'].append(ConstituentaSerializer(cst).data)
return result
class RSFormTRSSerializer(serializers.Serializer): class RSFormTRSSerializer(serializers.Serializer):
''' Serializer: TRS file production and loading for RSForm. ''' ''' Serializer: TRS file production and loading for RSForm. '''
class Meta:
''' serializer metadata. '''
model = RSForm
def to_representation(self, instance: RSForm) -> dict: def to_representation(self, instance: RSForm) -> dict:
result = self._prepare_json_rsform(instance) result = self._prepare_json_rsform(instance)
items = instance.constituents().order_by('order') items = instance.constituents().order_by('order')
@ -76,9 +72,9 @@ class RSFormTRSSerializer(serializers.Serializer):
def _prepare_json_rsform(schema: RSForm) -> dict: def _prepare_json_rsform(schema: RSForm) -> dict:
return { return {
'type': _TRS_TYPE, 'type': _TRS_TYPE,
'title': schema.title, 'title': schema.item.title,
'alias': schema.alias, 'alias': schema.item.alias,
'comment': schema.comment, 'comment': schema.item.comment,
'items': [], 'items': [],
'claimed': False, 'claimed': False,
'selection': [], 'selection': [],
@ -114,6 +110,8 @@ class RSFormTRSSerializer(serializers.Serializer):
result['owner'] = data['owner'] result['owner'] = data['owner']
if 'is_common' in data: if 'is_common' in data:
result['is_common'] = data['is_common'] result['is_common'] = data['is_common']
if 'is_canonical' in data:
result['is_canonical'] = data['is_canonical']
result['items'] = data.get('items', []) result['items'] = data.get('items', [])
if self.context['load_meta']: if self.context['load_meta']:
result['title'] = data.get('title', 'Без названия') result['title'] = data.get('title', 'Без названия')
@ -121,7 +119,7 @@ class RSFormTRSSerializer(serializers.Serializer):
result['comment']= data.get('comment', '') result['comment']= data.get('comment', '')
if 'id' in data: if 'id' in data:
result['id'] = data['id'] result['id'] = data['id']
self.instance = RSForm.objects.get(pk=result['id']) self.instance = RSForm(LibraryItem.objects.get(pk=result['id']))
return result return result
def validate(self, attrs: dict): def validate(self, attrs: dict):
@ -135,19 +133,20 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def create(self, validated_data: dict) -> RSForm: def create(self, validated_data: dict) -> RSForm:
self.instance = RSForm( self.instance: RSForm = RSForm.create(
owner=validated_data.get('owner', None), owner=validated_data.get('owner', None),
alias=validated_data['alias'], alias=validated_data['alias'],
title=validated_data['title'], title=validated_data['title'],
comment=validated_data['comment'], comment=validated_data['comment'],
is_common=validated_data['is_common'] is_common=validated_data['is_common'],
is_canonical=validated_data['is_canonical']
) )
self.instance.save() self.instance.item.save()
order = 1 order = 1
for cst_data in validated_data['items']: for cst_data in validated_data['items']:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=self.instance, schema=self.instance.item,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -160,11 +159,11 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def update(self, instance: RSForm, validated_data) -> RSForm: def update(self, instance: RSForm, validated_data) -> RSForm:
if 'alias' in validated_data: if 'alias' in validated_data:
instance.alias = validated_data['alias'] instance.item.alias = validated_data['alias']
if 'title' in validated_data: if 'title' in validated_data:
instance.title = validated_data['title'] instance.item.title = validated_data['title']
if 'comment' in validated_data: if 'comment' in validated_data:
instance.comment = validated_data['comment'] instance.item.comment = validated_data['comment']
order = 1 order = 1
prev_constituents = instance.constituents() prev_constituents = instance.constituents()
@ -181,7 +180,7 @@ class RSFormTRSSerializer(serializers.Serializer):
else: else:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=instance, schema=instance.item,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -196,7 +195,7 @@ class RSFormTRSSerializer(serializers.Serializer):
instance.update_order() instance.update_order()
instance.resolve_all_text() instance.resolve_all_text()
instance.save() instance.item.save()
return instance return instance
@staticmethod @staticmethod
@ -225,7 +224,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
def update(self, instance: Constituenta, validated_data) -> Constituenta: def update(self, instance: Constituenta, validated_data) -> Constituenta:
schema: RSForm = instance.schema schema = RSForm(instance.schema)
definition: Optional[str] = validated_data['definition_raw'] if 'definition_raw' in validated_data else None definition: Optional[str] = validated_data['definition_raw'] if 'definition_raw' in validated_data else None
term: Optional[str] = validated_data['term_raw'] if 'term_raw' in validated_data else None term: Optional[str] = validated_data['term_raw'] if 'term_raw' in validated_data else None
term_changed = False term_changed = False
@ -240,7 +239,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
if term_changed: if term_changed:
schema.on_term_change([result.alias]) schema.on_term_change([result.alias])
result.refresh_from_db() result.refresh_from_db()
schema.save() schema.item.save()
return result return result
@ -281,16 +280,16 @@ class CstRenameSerializer(serializers.ModelSerializer):
def validate(self, attrs): def validate(self, attrs):
schema = cast(RSForm, self.context['schema']) schema = cast(RSForm, self.context['schema'])
old_cst = Constituenta.objects.get(pk=self.initial_data['id']) old_cst = Constituenta.objects.get(pk=self.initial_data['id'])
if old_cst.schema != schema: if old_cst.schema != schema.item:
raise serializers.ValidationError({ raise serializers.ValidationError({
'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.title}' 'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}'
}) })
if old_cst.alias == self.initial_data['alias']: if old_cst.alias == self.initial_data['alias']:
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': f'Имя конституенты должно отличаться от текущего: {self.initial_data["alias"]}' 'alias': f'Имя конституенты должно отличаться от текущего: {self.initial_data["alias"]}'
}) })
self.instance = old_cst self.instance = old_cst
attrs['schema'] = schema attrs['schema'] = schema.item
attrs['id'] = self.initial_data['id'] attrs['id'] = self.initial_data['id']
return attrs return attrs
@ -306,7 +305,7 @@ class CstListSerializer(serializers.Serializer):
cstList = [] cstList = []
for item in attrs['items']: for item in attrs['items']:
cst = item['object'] cst = item['object']
if cst.schema != schema: if cst.schema != schema.item:
raise serializers.ValidationError( raise serializers.ValidationError(
{'items': f'Конституенты должны относиться к данной схеме: {item}'}) {'items': f'Конституенты должны относиться к данной схеме: {item}'})
cstList.append(cst) cstList.append(cst)

View File

@ -8,14 +8,17 @@ from apps.rsform.models import (
RSForm, RSForm,
Constituenta, Constituenta,
CstType, CstType,
User User,
LibraryItem,
LibraryItemType
) )
class TestConstituenta(TestCase): class TestConstituenta(TestCase):
''' Testing Constituenta model. '''
def setUp(self): def setUp(self):
self.schema1 = RSForm.objects.create(title='Test1') self.schema1 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test1')
self.schema2 = RSForm.objects.create(title='Test2') self.schema2 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test2')
def test_str(self): def test_str(self):
testStr = 'X1' testStr = 'X1'
@ -63,7 +66,8 @@ class TestConstituenta(TestCase):
self.assertEqual(cst.definition_raw, '') self.assertEqual(cst.definition_raw, '')
class TestRSForm(TestCase): class TestLibraryItem(TestCase):
''' Testing LibraryItem model. '''
def setUp(self): def setUp(self):
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') self.user2 = User.objects.create(username='User2')
@ -71,65 +75,79 @@ class TestRSForm(TestCase):
def test_str(self): def test_str(self):
testStr = 'Test123' testStr = 'Test123'
schema = RSForm.objects.create(title=testStr, owner=self.user1, alias='КС1') item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM,
self.assertEqual(str(schema), testStr) title=testStr, owner=self.user1, alias='КС1')
self.assertEqual(str(item), testStr)
def test_url(self): def test_url(self):
testStr = 'Test123' testStr = 'Test123'
schema = RSForm.objects.create(title=testStr, owner=self.user1, alias='КС1') item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM,
self.assertEqual(schema.get_absolute_url(), f'/api/rsforms/{schema.id}/') title=testStr, owner=self.user1, alias='КС1')
self.assertEqual(item.get_absolute_url(), f'/api/library/{item.id}/')
def test_create_default(self): def test_create_default(self):
schema = RSForm.objects.create(title='Test') item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
self.assertIsNone(schema.owner) self.assertIsNone(item.owner)
self.assertEqual(schema.title, 'Test') self.assertEqual(item.title, 'Test')
self.assertEqual(schema.alias, '') self.assertEqual(item.alias, '')
self.assertEqual(schema.comment, '') self.assertEqual(item.comment, '')
self.assertEqual(schema.is_common, False) self.assertEqual(item.is_common, False)
self.assertEqual(item.is_canonical, False)
def test_create(self): def test_create(self):
schema = RSForm.objects.create( item = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test', title='Test',
owner=self.user1, owner=self.user1,
alias='KS1', alias='KS1',
comment='Test comment', comment='Test comment',
is_common=True is_common=True,
is_canonical=True
) )
self.assertEqual(schema.owner, self.user1) self.assertEqual(item.owner, self.user1)
self.assertEqual(schema.title, 'Test') self.assertEqual(item.title, 'Test')
self.assertEqual(schema.alias, 'KS1') self.assertEqual(item.alias, 'KS1')
self.assertEqual(schema.comment, 'Test comment') self.assertEqual(item.comment, 'Test comment')
self.assertEqual(schema.is_common, True) self.assertEqual(item.is_common, True)
self.assertEqual(item.is_canonical, True)
class TestRSForm(TestCase):
''' Testing RSForm wrapper. '''
def setUp(self):
self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2')
self.assertNotEqual(self.user1, self.user2)
def test_constituents(self): def test_constituents(self):
schema1 = RSForm.objects.create(title='Test1') schema1 = RSForm.create(title='Test1')
schema2 = RSForm.objects.create(title='Test2') schema2 = RSForm.create(title='Test2')
self.assertFalse(schema1.constituents().exists()) self.assertFalse(schema1.constituents().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituents().exists())
Constituenta.objects.create(alias='X1', schema=schema1, order=1) Constituenta.objects.create(alias='X1', schema=schema1.item, order=1)
Constituenta.objects.create(alias='X2', schema=schema1, order=2) Constituenta.objects.create(alias='X2', schema=schema1.item, order=2)
self.assertTrue(schema1.constituents().exists()) self.assertTrue(schema1.constituents().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituents().exists())
self.assertEqual(schema1.constituents().count(), 2) self.assertEqual(schema1.constituents().count(), 2)
def test_insert_at(self): def test_insert_at(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
cst1 = schema.insert_at(1, 'X1', CstType.BASE) cst1 = schema.insert_at(1, 'X1', CstType.BASE)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
self.assertEqual(cst1.schema, schema) self.assertEqual(cst1.schema, schema.item)
cst2 = schema.insert_at(1, 'X2', CstType.BASE) cst2 = schema.insert_at(1, 'X2', CstType.BASE)
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst2.schema, schema) self.assertEqual(cst2.schema, schema.item)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
cst3 = schema.insert_at(4, 'X3', CstType.BASE) cst3 = schema.insert_at(4, 'X3', CstType.BASE)
cst2.refresh_from_db() cst2.refresh_from_db()
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst3.order, 3) self.assertEqual(cst3.order, 3)
self.assertEqual(cst3.schema, schema) self.assertEqual(cst3.schema, schema.item)
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
@ -138,7 +156,7 @@ class TestRSForm(TestCase):
cst2.refresh_from_db() cst2.refresh_from_db()
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst4.order, 3) self.assertEqual(cst4.order, 3)
self.assertEqual(cst4.schema, schema) self.assertEqual(cst4.schema, schema.item)
self.assertEqual(cst3.order, 4) self.assertEqual(cst3.order, 4)
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
@ -147,7 +165,7 @@ class TestRSForm(TestCase):
schema.insert_at(0, 'X5', CstType.BASE) schema.insert_at(0, 'X5', CstType.BASE)
def test_insert_at_reorder(self): def test_insert_at_reorder(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_at(1, 'X1', CstType.BASE)
d1 = schema.insert_at(2, 'D1', CstType.TERM) d1 = schema.insert_at(2, 'D1', CstType.TERM)
d2 = schema.insert_at(1, 'D2', CstType.TERM) d2 = schema.insert_at(1, 'D2', CstType.TERM)
@ -159,18 +177,18 @@ class TestRSForm(TestCase):
self.assertEqual(x2.order, 2) self.assertEqual(x2.order, 2)
def test_insert_last(self): def test_insert_last(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
cst1 = schema.insert_last('X1', CstType.BASE) cst1 = schema.insert_last('X1', CstType.BASE)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
self.assertEqual(cst1.schema, schema) self.assertEqual(cst1.schema, schema.item)
cst2 = schema.insert_last('X2', CstType.BASE) cst2 = schema.insert_last('X2', CstType.BASE)
self.assertEqual(cst2.order, 2) self.assertEqual(cst2.order, 2)
self.assertEqual(cst2.schema, schema) self.assertEqual(cst2.schema, schema.item)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
def test_create_cst_resolve(self): def test_create_cst_resolve(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
cst1 = schema.insert_last('X1', CstType.BASE) cst1 = schema.insert_last('X1', CstType.BASE)
cst1.term_raw = '@{X2|datv}' cst1.term_raw = '@{X2|datv}'
cst1.definition_raw = '@{X1|datv} @{X2|datv}' cst1.definition_raw = '@{X1|datv} @{X2|datv}'
@ -188,7 +206,7 @@ class TestRSForm(TestCase):
self.assertEqual(cst2.definition_resolved, 'слонам слоны') self.assertEqual(cst2.definition_resolved, 'слонам слоны')
def test_delete_cst(self): def test_delete_cst(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_last('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM) d1 = schema.insert_last('D1', CstType.TERM)
@ -196,13 +214,13 @@ class TestRSForm(TestCase):
schema.delete_cst([x2, d1]) schema.delete_cst([x2, d1])
x1.refresh_from_db() x1.refresh_from_db()
d2.refresh_from_db() d2.refresh_from_db()
schema.refresh_from_db() schema.item.refresh_from_db()
self.assertEqual(schema.constituents().count(), 2) self.assertEqual(schema.constituents().count(), 2)
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
self.assertEqual(d2.order, 2) self.assertEqual(d2.order, 2)
def test_move_cst(self): def test_move_cst(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_last('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM) d1 = schema.insert_last('D1', CstType.TERM)
@ -218,7 +236,7 @@ class TestRSForm(TestCase):
self.assertEqual(d2.order, 3) self.assertEqual(d2.order, 3)
def test_move_cst_down(self): def test_move_cst_down(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_last('X2', CstType.BASE)
schema.move_cst([x1], 2) schema.move_cst([x1], 2)
@ -228,7 +246,7 @@ class TestRSForm(TestCase):
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
def test_reset_aliases(self): def test_reset_aliases(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X11', CstType.BASE) x1 = schema.insert_last('X11', CstType.BASE)
x2 = schema.insert_last('X21', CstType.BASE) x2 = schema.insert_last('X21', CstType.BASE)
d1 = schema.insert_last('D11', CstType.TERM) d1 = schema.insert_last('D11', CstType.TERM)

View File

@ -9,7 +9,7 @@ from rest_framework.exceptions import ErrorDetail
from cctext import ReferenceType from cctext import ReferenceType
from apps.users.models import User from apps.users.models import User
from apps.rsform.models import Syntax, RSForm, Constituenta, CstType from apps.rsform.models import Syntax, RSForm, Constituenta, CstType, LibraryItem, LibraryItemType
from apps.rsform.views import ( from apps.rsform.views import (
convert_to_ascii, convert_to_ascii,
convert_to_math, convert_to_math,
@ -17,28 +17,28 @@ from apps.rsform.views import (
) )
def _response_contains(response, schema: RSForm) -> bool: def _response_contains(response, item: LibraryItem) -> bool:
return any(x for x in response.data if x['id'] == schema.pk) return any(x for x in response.data if x['id'] == item.pk)
class TestConstituentaAPI(APITestCase): class TestConstituentaAPI(APITestCase):
''' Testing constituenta view. ''' ''' Testing Constituenta view. '''
def setUp(self): def setUp(self):
self.factory = APIRequestFactory() self.factory = APIRequestFactory()
self.user = User.objects.create(username='UserTest') self.user = User.objects.create(username='UserTest')
self.client = APIClient() self.client = APIClient()
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2') self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create( self.cst1 = Constituenta.objects.create(
alias='X1', schema=self.rsform_owned, order=1, convention='Test', alias='X1', schema=self.rsform_owned.item, order=1, convention='Test',
term_raw='Test1', term_resolved='Test1R', term_raw='Test1', term_resolved='Test1R',
term_forms=[{'text':'form1', 'tags':'sing,datv'}]) term_forms=[{'text':'form1', 'tags':'sing,datv'}])
self.cst2 = Constituenta.objects.create( self.cst2 = Constituenta.objects.create(
alias='X2', schema=self.rsform_unowned, order=1, convention='Test1', alias='X2', schema=self.rsform_unowned.item, order=1, convention='Test1',
term_raw='Test2', term_resolved='Test2R') term_raw='Test2', term_resolved='Test2R')
self.cst3 = Constituenta.objects.create( self.cst3 = Constituenta.objects.create(
alias='X3', schema=self.rsform_owned, order=2, alias='X3', schema=self.rsform_owned.item, order=2,
term_raw='Test3', term_resolved='Test3', term_raw='Test3', term_resolved='Test3',
definition_raw='Test1', definition_resolved='Test2') definition_raw='Test1', definition_resolved='Test2')
@ -102,6 +102,110 @@ class TestConstituentaAPI(APITestCase):
self.assertEqual(response.data['order'], self.cst1.order) self.assertEqual(response.data['order'], self.cst1.order)
class TestLibraryViewset(APITestCase):
''' Testing Library view. '''
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create(username='UserTest')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
self.owned = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test',
alias='T1',
owner=self.user
)
self.unowned = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test2',
alias='T2'
)
self.common = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test3',
alias='T3',
is_common=True
)
def test_create_anonymous(self):
self.client.logout()
data = json.dumps({'title': 'Title'})
response = self.client.post('/api/library/', data=data, content_type='application/json')
self.assertEqual(response.status_code, 403)
def test_create_populate_user(self):
data = json.dumps({'title': 'Title'})
response = self.client.post('/api/library/', data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['title'], 'Title')
self.assertEqual(response.data['owner'], self.user.id)
def test_update(self):
data = json.dumps({'id': self.owned.id, 'title': 'New title'})
response = self.client.patch(f'/api/library/{self.owned.id}/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'New title')
self.assertEqual(response.data['alias'], self.owned.alias)
def test_update_unowned(self):
data = json.dumps({'id': self.unowned.id, 'title': 'New title'})
response = self.client.patch(f'/api/library/{self.unowned.id}/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 403)
def test_destroy(self):
response = self.client.delete(f'/api/library/{self.owned.id}/')
self.assertTrue(response.status_code in [202, 204])
def test_destroy_admin_override(self):
response = self.client.delete(f'/api/library/{self.unowned.id}/')
self.assertEqual(response.status_code, 403)
self.user.is_staff = True
self.user.save()
response = self.client.delete(f'/api/library/{self.unowned.id}/')
self.assertTrue(response.status_code in [202, 204])
def test_claim(self):
response = self.client.post(f'/api/library/{self.owned.id}/claim/')
self.assertEqual(response.status_code, 403)
self.owned.is_common = True
self.owned.save()
response = self.client.post(f'/api/library/{self.owned.id}/claim/')
self.assertEqual(response.status_code, 304)
response = self.client.post(f'/api/library/{self.unowned.id}/claim/')
self.assertEqual(response.status_code, 403)
self.unowned.is_common = True
self.unowned.save()
response = self.client.post(f'/api/library/{self.unowned.id}/claim/')
self.assertEqual(response.status_code, 200)
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.owner, self.user)
def test_claim_anonymous(self):
self.client.logout()
response = self.client.post(f'/api/library/{self.owned.id}/claim/')
self.assertEqual(response.status_code, 403)
def test_retrieve_common(self):
self.client.logout()
response = self.client.get('/api/library/active/')
self.assertEqual(response.status_code, 200)
self.assertTrue(_response_contains(response, self.common))
self.assertFalse(_response_contains(response, self.unowned))
self.assertFalse(_response_contains(response, self.owned))
def test_retrieve_owned(self):
response = self.client.get('/api/library/active/')
self.assertEqual(response.status_code, 200)
self.assertTrue(_response_contains(response, self.common))
self.assertFalse(_response_contains(response, self.unowned))
self.assertTrue(_response_contains(response, self.owned))
class TestRSFormViewset(APITestCase): class TestRSFormViewset(APITestCase):
''' Testing RSForm view. ''' ''' Testing RSForm view. '''
def setUp(self): def setUp(self):
@ -109,56 +213,34 @@ class TestRSFormViewset(APITestCase):
self.user = User.objects.create(username='UserTest') self.user = User.objects.create(username='UserTest')
self.client = APIClient() self.client = APIClient()
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user) self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2') self.unowned = RSForm.create(title='Test2', alias='T2')
def test_create_anonymous(self): def test_list(self):
self.client.logout() non_schema = LibraryItem.objects.create(
data = json.dumps({'title': 'Title'}) item_type=LibraryItemType.OPERATIONS_SCHEMA,
response = self.client.post('/api/rsforms/', data=data, content_type='application/json') title='Test3'
self.assertEqual(response.status_code, 403) )
response = self.client.get('/api/rsforms/')
def test_create_populate_user(self):
data = json.dumps({'title': 'Title'})
response = self.client.post('/api/rsforms/', data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['title'], 'Title')
self.assertEqual(response.data['owner'], self.user.id)
def test_update(self):
data = json.dumps({'id': self.rsform_owned.id, 'title': 'New title'})
response = self.client.patch(f'/api/rsforms/{self.rsform_owned.id}/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'New title') self.assertFalse(_response_contains(response, non_schema))
self.assertEqual(response.data['alias'], self.rsform_owned.alias) self.assertTrue(_response_contains(response, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item))
def test_update_unowned(self): response = self.client.get('/api/library/')
data = json.dumps({'id': self.rsform_unowned.id, 'title': 'New title'}) self.assertEqual(response.status_code, 200)
response = self.client.patch(f'/api/rsforms/{self.rsform_unowned.id}/', self.assertTrue(_response_contains(response, non_schema))
data=data, content_type='application/json') self.assertTrue(_response_contains(response, self.unowned.item))
self.assertEqual(response.status_code, 403) self.assertTrue(_response_contains(response, self.owned.item))
def test_destroy(self):
response = self.client.delete(f'/api/rsforms/{self.rsform_owned.id}/')
self.assertTrue(response.status_code in [202, 204])
def test_destroy_admin_override(self):
response = self.client.delete(f'/api/rsforms/{self.rsform_unowned.id}/')
self.assertEqual(response.status_code, 403)
self.user.is_staff = True
self.user.save()
response = self.client.delete(f'/api/rsforms/{self.rsform_unowned.id}/')
self.assertTrue(response.status_code in [202, 204])
def test_contents(self): def test_contents(self):
schema = RSForm.objects.create(title='Title1') schema = RSForm.create(title='Title1')
schema.insert_last(alias='X1', insert_type=CstType.BASE) schema.insert_last(alias='X1', insert_type=CstType.BASE)
response = self.client.get(f'/api/rsforms/{schema.id}/contents/') response = self.client.get(f'/api/rsforms/{schema.item.id}/contents/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_details(self): def test_details(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_at(1, 'X1', CstType.BASE) x1 = schema.insert_at(1, 'X1', CstType.BASE)
x2 = schema.insert_at(2, 'X2', CstType.BASE) x2 = schema.insert_at(2, 'X2', CstType.BASE)
x1.term_raw = 'человек' x1.term_raw = 'человек'
@ -168,7 +250,7 @@ class TestRSFormViewset(APITestCase):
x1.save() x1.save()
x2.save() x2.save()
response = self.client.get(f'/api/rsforms/{schema.id}/details/') response = self.client.get(f'/api/rsforms/{schema.item.id}/details/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'Test') self.assertEqual(response.data['title'], 'Test')
@ -182,10 +264,10 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['items'][1]['term']['resolved'], x2.term_resolved) self.assertEqual(response.data['items'][1]['term']['resolved'], x2.term_resolved)
def test_check(self): def test_check(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_at(1, 'X1', CstType.BASE)
data = json.dumps({'expression': 'X1=X1'}) data = json.dumps({'expression': 'X1=X1'})
response = self.client.post(f'/api/rsforms/{schema.id}/check/', data=data, content_type='application/json') response = self.client.post(f'/api/rsforms/{schema.item.id}/check/', data=data, content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], Syntax.MATH) self.assertEqual(response.data['syntax'], Syntax.MATH)
@ -194,12 +276,12 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['valueClass'], 'value') self.assertEqual(response.data['valueClass'], 'value')
def test_resolve(self): def test_resolve(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_at(1, 'X1', CstType.BASE) x1 = schema.insert_at(1, 'X1', CstType.BASE)
x1.term_resolved = 'синий слон' x1.term_resolved = 'синий слон'
x1.save() x1.save()
data = json.dumps({'text': '@{1|редкий} @{X1|plur,datv}'}) data = json.dumps({'text': '@{1|редкий} @{X1|plur,datv}'})
response = self.client.post(f'/api/rsforms/{schema.id}/resolve/', data=data, content_type='application/json') response = self.client.post(f'/api/rsforms/{schema.item.id}/resolve/', data=data, content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
self.assertEqual(response.data['output'], 'редким синим слонам') self.assertEqual(response.data['output'], 'редким синим слонам')
@ -231,9 +313,9 @@ class TestRSFormViewset(APITestCase):
self.assertTrue(response.data['title'] != '') self.assertTrue(response.data['title'] != '')
def test_export_trs(self): def test_export_trs(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_at(1, 'X1', CstType.BASE)
response = self.client.get(f'/api/rsforms/{schema.id}/export-trs/') response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
with io.BytesIO(response.content) as stream: with io.BytesIO(response.content) as stream:
@ -241,29 +323,16 @@ class TestRSFormViewset(APITestCase):
self.assertIsNone(zipped_file.testzip()) self.assertIsNone(zipped_file.testzip())
self.assertIn('document.json', zipped_file.namelist()) self.assertIn('document.json', zipped_file.namelist())
def test_claim(self):
response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/')
self.assertEqual(response.status_code, 304)
response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/claim/')
self.assertEqual(response.status_code, 200)
self.rsform_unowned.refresh_from_db()
self.assertEqual(self.rsform_unowned.owner, self.user)
def test_claim_anonymous(self):
self.client.logout()
response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/')
self.assertEqual(response.status_code, 403)
def test_create_constituenta(self): def test_create_constituenta(self):
data = json.dumps({'alias': 'X3', 'cst_type': 'basic'}) data = json.dumps({'alias': 'X3', 'cst_type': 'basic'})
response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/cst-create/', response = self.client.post(f'/api/rsforms/{self.unowned.item.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
schema = self.rsform_owned item = self.owned.item
Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1) Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/', response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['alias'], 'X3')
@ -271,7 +340,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x3.order, 3) self.assertEqual(x3.order, 3)
data = json.dumps({'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id}) data = json.dumps({'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id})
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/', response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X4') self.assertEqual(response.data['new_cst']['alias'], 'X4')
@ -280,34 +349,34 @@ class TestRSFormViewset(APITestCase):
def test_rename_constituenta(self): def test_rename_constituenta(self):
self.cst1 = Constituenta.objects.create( self.cst1 = Constituenta.objects.create(
alias='X1', schema=self.rsform_owned, order=1, convention='Test', alias='X1', schema=self.owned.item, order=1, convention='Test',
term_raw='Test1', term_resolved='Test1', term_raw='Test1', term_resolved='Test1',
term_forms=[{'text':'form1', 'tags':'sing,datv'}]) term_forms=[{'text':'form1', 'tags':'sing,datv'}])
self.cst2 = Constituenta.objects.create( self.cst2 = Constituenta.objects.create(
alias='X2', schema=self.rsform_unowned, order=1, convention='Test1', alias='X2', schema=self.unowned.item, order=1, convention='Test1',
term_raw='Test2', term_resolved='Test2') term_raw='Test2', term_resolved='Test2')
self.cst3 = Constituenta.objects.create( self.cst3 = Constituenta.objects.create(
alias='X3', schema=self.rsform_owned, order=2, alias='X3', schema=self.owned.item, order=2,
term_raw='Test3', term_resolved='Test3', term_raw='Test3', term_resolved='Test3',
definition_raw='Test1', definition_resolved='Test2') definition_raw='Test1', definition_resolved='Test2')
data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk}) data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk})
response = self.client.patch(f'/api/rsforms/{self.rsform_unowned.id}/cst-rename/', response = self.client.patch(f'/api/rsforms/{self.unowned.item.id}/cst-rename/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = self.client.patch(f'/api/rsforms/{self.rsform_owned.id}/cst-rename/', response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = json.dumps({'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk}) data = json.dumps({'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk})
response = self.client.patch(f'/api/rsforms/{self.rsform_owned.id}/cst-rename/', response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk}) data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk})
schema = self.rsform_owned item = self.owned.item
d1 = Constituenta.objects.create(schema=schema, alias='D1', cst_type='term', order=4) d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4)
d1.term_raw = '@{X1|plur}' d1.term_raw = '@{X1|plur}'
d1.definition_formal = 'X1' d1.definition_formal = 'X1'
d1.save() d1.save()
@ -315,7 +384,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(self.cst1.order, 1) self.assertEqual(self.cst1.order, 1)
self.assertEqual(self.cst1.alias, 'X1') self.assertEqual(self.cst1.alias, 'X1')
self.assertEqual(self.cst1.cst_type, CstType.BASE) self.assertEqual(self.cst1.cst_type, CstType.BASE)
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-rename/', response = self.client.patch(f'/api/rsforms/{item.id}/cst-rename/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['new_cst']['alias'], 'D2') self.assertEqual(response.data['new_cst']['alias'], 'D2')
@ -337,8 +406,8 @@ class TestRSFormViewset(APITestCase):
'definition_formal': '3', 'definition_formal': '3',
'definition_raw': '4' 'definition_raw': '4'
}) })
schema = self.rsform_owned item = self.owned.item
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/', response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['alias'], 'X3')
@ -351,66 +420,66 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['new_cst']['definition_resolved'], '4') self.assertEqual(response.data['new_cst']['definition_resolved'], '4')
def test_delete_constituenta(self): def test_delete_constituenta(self):
schema = self.rsform_owned schema = self.owned
data = json.dumps({'items': [{'id': 1337}]}) data = json.dumps({'items': [{'id': 1337}]})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/', response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1) x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2) x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2)
data = json.dumps({'items': [{'id': x1.id}]}) data = json.dumps({'items': [{'id': x1.id}]})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/', response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete/',
data=data, content_type='application/json') data=data, content_type='application/json')
x2.refresh_from_db() x2.refresh_from_db()
schema.refresh_from_db() schema.item.refresh_from_db()
self.assertEqual(response.status_code, 202) self.assertEqual(response.status_code, 202)
self.assertEqual(len(response.data['items']), 1) self.assertEqual(len(response.data['items']), 1)
self.assertEqual(schema.constituents().count(), 1) self.assertEqual(schema.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2') self.assertEqual(x2.alias, 'X2')
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', cst_type='basic', order=1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1)
data = json.dumps({'items': [{'id': x3.id}]}) data = json.dumps({'items': [{'id': x3.id}]})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/', response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_move_constituenta(self): def test_move_constituenta(self):
schema = self.rsform_owned item = self.owned.item
data = json.dumps({'items': [{'id': 1337}], 'move_to': 1}) data = json.dumps({'items': [{'id': 1337}], 'move_to': 1})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/', response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1) x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
data = json.dumps({'items': [{'id': x2.id}], 'move_to': 1}) data = json.dumps({'items': [{'id': x2.id}], 'move_to': 1})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/', response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto/',
data=data, content_type='application/json') data=data, content_type='application/json')
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], schema.id) self.assertEqual(response.data['id'], item.id)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', cst_type='basic', order=1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1)
data = json.dumps({'items': [{'id': x3.id}], 'move_to': 1}) data = json.dumps({'items': [{'id': x3.id}], 'move_to': 1})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/', response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_reset_aliases(self): def test_reset_aliases(self):
schema = self.rsform_owned item = self.owned.item
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/') response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['id'], schema.id) self.assertEqual(response.data['id'], item.id)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=1) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=1)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=2) x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=2)
d11 = Constituenta.objects.create(schema=schema, alias='D11', cst_type='term', order=3) d11 = Constituenta.objects.create(schema=item, alias='D11', cst_type='term', order=3)
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/') response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
d11.refresh_from_db() d11.refresh_from_db()
@ -422,31 +491,31 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(d11.order, 3) self.assertEqual(d11.order, 3)
self.assertEqual(d11.alias, 'D1') self.assertEqual(d11.alias, 'D1')
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/') response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_load_trs(self): def test_load_trs(self):
schema = self.rsform_owned schema = self.owned
schema.title = 'Testt11' schema.item.title = 'Testt11'
schema.save() schema.item.save()
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1) x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1)
work_dir = os.path.dirname(os.path.abspath(__file__)) work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False} data = {'file': file, 'load_metadata': False}
response = self.client.patch(f'/api/rsforms/{schema.id}/load-trs/', data=data, format='multipart') response = self.client.patch(f'/api/rsforms/{schema.item.id}/load-trs/', data=data, format='multipart')
schema.refresh_from_db() schema.item.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(schema.title, 'Testt11') self.assertEqual(schema.item.title, 'Testt11')
self.assertEqual(len(response.data['items']), 25) self.assertEqual(len(response.data['items']), 25)
self.assertEqual(schema.constituents().count(), 25) self.assertEqual(schema.constituents().count(), 25)
self.assertFalse(Constituenta.objects.all().filter(pk=x1.id).exists()) self.assertFalse(Constituenta.objects.all().filter(pk=x1.id).exists())
def test_clone(self): def test_clone(self):
schema = self.rsform_owned item = self.owned.item
schema.title = 'Testt11' item.title = 'Testt11'
schema.save() item.save()
x1 = Constituenta.objects.create(schema=schema, alias='X12', cst_type='basic', order=1) x1 = Constituenta.objects.create(schema=item, alias='X12', cst_type='basic', order=1)
d1 = Constituenta.objects.create(schema=schema, alias='D2', cst_type='term', order=1) d1 = Constituenta.objects.create(schema=item, alias='D2', cst_type='term', order=1)
x1.term_raw = 'человек' x1.term_raw = 'человек'
x1.term_resolved = 'человек' x1.term_resolved = 'человек'
d1.term_raw = '@{X12|plur}' d1.term_raw = '@{X12|plur}'
@ -455,7 +524,7 @@ class TestRSFormViewset(APITestCase):
d1.save() d1.save()
data = json.dumps({'title': 'Title'}) data = json.dumps({'title': 'Title'})
response = self.client.post(f'/api/rsforms/{schema.id}/clone/', data=data, content_type='application/json') response = self.client.post(f'/api/library/{item.id}/clone/', data=data, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['title'], 'Title')
@ -536,29 +605,3 @@ class TestFunctionalViews(APITestCase):
response = parse_expression(request) response = parse_expression(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
class TestLibraryAPI(APITestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create(username='UserTest')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
self.rsform_common = RSForm.objects.create(title='Test3', alias='T3', is_common=True)
def test_retrieve_common(self):
self.client.logout()
response = self.client.get('/api/library/')
self.assertEqual(response.status_code, 200)
self.assertTrue(_response_contains(response, self.rsform_common))
self.assertFalse(_response_contains(response, self.rsform_unowned))
self.assertFalse(_response_contains(response, self.rsform_owned))
def test_retrieve_owned(self):
response = self.client.get('/api/library/')
self.assertEqual(response.status_code, 200)
self.assertTrue(_response_contains(response, self.rsform_common))
self.assertFalse(_response_contains(response, self.rsform_unowned))
self.assertTrue(_response_contains(response, self.rsform_owned))

View File

@ -3,16 +3,17 @@ from django.urls import path, include
from rest_framework import routers from rest_framework import routers
from . import views from . import views
rsform_router = routers.SimpleRouter() library_router = routers.SimpleRouter()
rsform_router.register(r'rsforms', views.RSFormViewSet) library_router.register('library', views.LibraryViewSet)
library_router.register('rsforms', views.RSFormViewSet)
urlpatterns = [ urlpatterns = [
path('library/', views.LibraryView.as_view(), name='library'), path('library/active/', views.LibraryActiveView.as_view(), name='library'),
path('constituents/<int:pk>/', views.ConstituentAPIView.as_view(), name='constituenta-detail'), path('constituents/<int:pk>/', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
path('rsforms/import-trs/', views.TrsImportView.as_view()), path('rsforms/import-trs/', views.TrsImportView.as_view()),
path('rsforms/create-detailed/', views.create_rsform), path('rsforms/create-detailed/', views.create_rsform),
path('func/parse-expression/', views.parse_expression), path('func/parse-expression/', views.parse_expression),
path('func/to-ascii/', views.convert_to_ascii), path('func/to-ascii/', views.convert_to_ascii),
path('func/to-math/', views.convert_to_math), path('func/to-math/', views.convert_to_math),
path('', include(rsform_router.urls)), path('', include(library_router.urls)),
] ]

View File

@ -3,7 +3,7 @@ import json
from io import BytesIO from io import BytesIO
import re import re
from zipfile import ZipFile from zipfile import ZipFile
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission, IsAuthenticated
class ObjectOwnerOrAdmin(BasePermission): class ObjectOwnerOrAdmin(BasePermission):
@ -16,6 +16,14 @@ class ObjectOwnerOrAdmin(BasePermission):
return request.user.is_staff # type: ignore return request.user.is_staff # type: ignore
class IsClaimable(IsAuthenticated):
''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj):
if not super().has_permission(request, view):
return False
return obj.is_common
class SchemaOwnerOrAdmin(BasePermission): class SchemaOwnerOrAdmin(BasePermission):
''' Permission for object ownership restriction ''' ''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):

View File

@ -1,5 +1,6 @@
''' REST API: RSForms for conceptual schemas. ''' ''' REST API: RSForms for conceptual schemas. '''
import json import json
from typing import cast
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
@ -10,28 +11,28 @@ from rest_framework.response import Response
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
import pyconcept import pyconcept
from . import models from . import models as m
from . import serializers from . import serializers as s
from . import utils from . import utils
class LibraryView(generics.ListAPIView): class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of rsforms available for active user. ''' ''' Endpoint: Get list of rsforms available for active user. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.RSFormMetaSerializer serializer_class = s.LibraryItemSerializer
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
if not user.is_anonymous: if not user.is_anonymous:
return models.RSForm.objects.filter(Q(is_common=True) | Q(owner=user)) return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user))
else: else:
return models.RSForm.objects.filter(is_common=True) return m.LibraryItem.objects.filter(is_common=True)
class ConstituentAPIView(generics.RetrieveUpdateAPIView): class ConstituentAPIView(generics.RetrieveUpdateAPIView):
''' Endpoint: Get / Update Constituenta. ''' ''' Endpoint: Get / Update Constituenta. '''
queryset = models.Constituenta.objects.all() queryset = m.Constituenta.objects.all()
serializer_class = serializers.ConstituentaSerializer serializer_class = s.ConstituentaSerializer
def get_permissions(self): def get_permissions(self):
result = super().get_permissions() result = super().get_permissions()
@ -41,21 +42,17 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView):
result.append(utils.SchemaOwnerOrAdmin()) result.append(utils.SchemaOwnerOrAdmin())
return result return result
# pylint: disable=too-many-ancestors # pylint: disable=too-many-ancestors
class RSFormViewSet(viewsets.ModelViewSet): class LibraryViewSet(viewsets.ModelViewSet):
''' Endpoint: RSForm operations. ''' ''' Endpoint: Library operations. '''
queryset = models.RSForm.objects.all() queryset = m.LibraryItem.objects.all()
serializer_class = serializers.RSFormMetaSerializer serializer_class = s.LibraryItemSerializer
filter_backends = (DjangoFilterBackend, filters.OrderingFilter) filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_fields = ['owner', 'is_common'] filterset_fields = ['item_type', 'owner', 'is_common', 'is_canonical']
ordering_fields = ('owner', 'title', 'time_update') ordering_fields = ('item_type', 'owner', 'title', 'time_update')
ordering = '-time_update' ordering = '-time_update'
def _get_schema(self) -> models.RSForm:
return self.get_object() # type: ignore
def perform_create(self, serializer): def perform_create(self, serializer):
if not self.request.user.is_anonymous and 'owner' not in self.request.POST: if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
return serializer.save(owner=self.request.user) return serializer.save(owner=self.request.user)
@ -63,11 +60,64 @@ class RSFormViewSet(viewsets.ModelViewSet):
return serializer.save() return serializer.save()
def get_permissions(self): def get_permissions(self):
if self.action in ['update', 'destroy', 'partial_update', 'load_trs', if self.action in ['update', 'destroy', 'partial_update']:
'cst_create', 'cst_multidelete', 'reset_aliases', 'cst_rename']:
permission_classes = [utils.ObjectOwnerOrAdmin] permission_classes = [utils.ObjectOwnerOrAdmin]
elif self.action in ['create', 'claim', 'clone']: elif self.action in ['create', 'clone']:
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
elif self.action in ['claim']:
permission_classes = [utils.IsClaimable]
else:
permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes]
@transaction.atomic
@action(detail=True, methods=['post'], url_path='clone')
def clone(self, request, pk):
''' Endpoint: Create deep copy of library item. '''
serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
item = cast(m.LibraryItem, self.get_object())
if item.item_type == m.LibraryItemType.RSFORM:
schema = m.RSForm(item)
clone_data = s.RSFormTRSSerializer(schema).data
clone_data['item_type'] = item.item_type
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_data['is_canonical'] = serializer.validated_data.get('is_canonical', False)
clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
clone.is_valid(raise_exception=True)
new_schema = clone.save()
return Response(status=201, data=m.PyConceptAdapter(new_schema).full())
return Response(status=404)
@action(detail=True, methods=['post'])
def claim(self, request, pk=None):
''' Endpoint: Claim ownership of LibraryItem. '''
item = cast(m.LibraryItem, self.get_object())
if item.owner == self.request.user:
return Response(status=304)
else:
item.owner = self.request.user
item.save()
return Response(status=200, data=s.LibraryItemSerializer(item).data)
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. '''
queryset = m.LibraryItem.objects.all().filter(item_type=m.LibraryItemType.RSFORM)
serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.RSForm:
return m.RSForm(self.get_object()) # type: ignore
def get_permissions(self):
''' Determine permission class. '''
if self.action in ['load_trs', 'cst_create', 'cst_multidelete',
'reset_aliases', 'cst_rename']:
permission_classes = [utils.ObjectOwnerOrAdmin]
else: else:
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes] return [permission() for permission in permission_classes]
@ -76,14 +126,14 @@ class RSFormViewSet(viewsets.ModelViewSet):
def cst_create(self, request, pk): def cst_create(self, request, pk):
''' Create new constituenta. ''' ''' Create new constituenta. '''
schema = self._get_schema() schema = self._get_schema()
serializer = serializers.CstCreateSerializer(data=request.data) serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
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.item.refresh_from_db()
response = Response(status=201, data={ response = Response(status=201, data={
'new_cst': serializers.ConstituentaSerializer(new_cst).data, 'new_cst': s.ConstituentaSerializer(new_cst).data,
'schema': models.PyConceptAdapter(schema).full() 'schema': m.PyConceptAdapter(schema).full()
}) })
response['Location'] = new_cst.get_absolute_url() response['Location'] = new_cst.get_absolute_url()
return response return response
@ -93,109 +143,80 @@ class RSFormViewSet(viewsets.ModelViewSet):
def cst_rename(self, request, pk): def cst_rename(self, request, pk):
''' Rename constituenta possibly changing type. ''' ''' Rename constituenta possibly changing type. '''
schema = self._get_schema() schema = self._get_schema()
serializer = serializers.CstRenameSerializer(data=request.data, context={'schema': schema}) serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
old_alias = models.Constituenta.objects.get(pk=request.data['id']).alias old_alias = m.Constituenta.objects.get(pk=request.data['id']).alias
serializer.save() serializer.save()
mapping = { old_alias: serializer.validated_data['alias'] } mapping = { old_alias: serializer.validated_data['alias'] }
schema.apply_mapping(mapping, change_aliases=False) schema.apply_mapping(mapping, change_aliases=False)
schema.update_order() schema.update_order()
schema.refresh_from_db() schema.item.refresh_from_db()
cst = models.Constituenta.objects.get(pk=serializer.validated_data['id']) cst = m.Constituenta.objects.get(pk=serializer.validated_data['id'])
return Response(status=200, data={ return Response(status=200, data={
'new_cst': serializers.ConstituentaSerializer(cst).data, 'new_cst': s.ConstituentaSerializer(cst).data,
'schema': models.PyConceptAdapter(schema).full() 'schema': m.PyConceptAdapter(schema).full()
}) })
@action(detail=True, methods=['patch'], url_path='cst-multidelete') @action(detail=True, methods=['patch'], url_path='cst-multidelete')
def cst_multidelete(self, request, pk): def cst_multidelete(self, request, pk):
''' Endpoint: Delete multiple constituents. ''' ''' Endpoint: Delete multiple constituents. '''
schema = self._get_schema() schema = self._get_schema()
serializer = serializers.CstListSerializer(data=request.data, context={'schema': schema}) serializer = s.CstListSerializer(data=request.data, context={'schema': schema})
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.item.refresh_from_db()
return Response(status=202, data=models.PyConceptAdapter(schema).full()) return Response(status=202, data=m.PyConceptAdapter(schema).full())
@action(detail=True, methods=['patch'], url_path='cst-moveto') @action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request, pk): def cst_moveto(self, request, pk):
''' Endpoint: Move multiple constituents. ''' ''' Endpoint: Move multiple constituents. '''
schema = self._get_schema() schema = self._get_schema()
serializer = serializers.CstMoveSerializer(data=request.data, context={'schema': schema}) serializer = s.CstMoveSerializer(data=request.data, context={'schema': schema})
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.item.refresh_from_db()
return Response(status=200, data=models.PyConceptAdapter(schema).full()) return Response(status=200, data=m.PyConceptAdapter(schema).full())
@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()
schema.reset_aliases() schema.reset_aliases()
return Response(status=200, data=models.PyConceptAdapter(schema).full()) return Response(status=200, data=m.PyConceptAdapter(schema).full())
@action(detail=True, methods=['patch'], url_path='load-trs') @action(detail=True, methods=['patch'], url_path='load-trs')
def load_trs(self, request, pk): def load_trs(self, request, pk):
''' Endpoint: Load data from file and replace current schema. ''' ''' Endpoint: Load data from file and replace current schema. '''
serializer = serializers.RSFormUploadSerializer(data=request.data) serializer = s.RSFormUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
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)
data['id'] = schema.pk data['id'] = schema.item.pk
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata}) serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer.save()
return Response(status=200, data=models.PyConceptAdapter(schema).full()) return Response(status=200, data=m.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.RSFormMetaSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
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):
''' Endpoint: Claim ownership of RSForm. '''
schema = self._get_schema()
if schema.owner == self.request.user:
return Response(status=304)
else:
schema.owner = self.request.user
schema.save()
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):
''' Endpoint: View schema db contents (including constituents). ''' ''' Endpoint: View schema db contents (including constituents). '''
schema = serializers.RSFormContentsSerializer(self._get_schema()).data schema = s.RSFormSerializer(self._get_schema()).data
return Response(schema) return Response(schema)
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def details(self, request, pk): def details(self, request, pk):
''' Endpoint: Detailed schema view including statuses and parse. ''' ''' Endpoint: Detailed schema view including statuses and parse. '''
schema = self._get_schema() schema = self._get_schema()
serializer = models.PyConceptAdapter(schema) serializer = m.PyConceptAdapter(schema)
return Response(serializer.full()) 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 = models.PyConceptAdapter(self._get_schema()) schema = m.PyConceptAdapter(self._get_schema())
serializer = serializers.ExpressionSerializer(data=request.data) serializer = s.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.data), expression) result = pyconcept.check_expression(json.dumps(schema.data), expression)
@ -205,19 +226,19 @@ class RSFormViewSet(viewsets.ModelViewSet):
def resolve(self, request, pk): def resolve(self, request, pk):
''' Endpoint: Resolve refenrces in text against schema terms context. ''' ''' Endpoint: Resolve refenrces in text against schema terms context. '''
schema = self._get_schema() schema = self._get_schema()
serializer = serializers.TextSerializer(data=request.data) serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text'] text = serializer.validated_data['text']
resolver = schema.resolver() resolver = schema.resolver()
resolver.resolve(text) resolver.resolve(text)
return Response(status=200, data=serializers.ResolverSerializer(resolver).data) return Response(status=200, data=s.ResolverSerializer(resolver).data)
@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 = serializers.RSFormTRSSerializer(self._get_schema()).data schema = s.RSFormTRSSerializer(self._get_schema()).data
trs = utils.write_trs(schema) trs = utils.write_trs(schema)
filename = self._get_schema().alias filename = self._get_schema().item.alias
if filename == '' or not filename.isascii(): if filename == '' or not filename.isascii():
# Note: non-ascii symbols in Content-Disposition # Note: non-ascii symbols in Content-Disposition
# are not supported by some browsers # are not supported by some browsers
@ -231,7 +252,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
class TrsImportView(views.APIView): class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. ''' ''' Endpoint: Upload RS form in Exteor format. '''
serializer_class = serializers.FileSerializer serializer_class = s.FileSerializer
def post(self, request): def post(self, request):
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
@ -239,10 +260,10 @@ class TrsImportView(views.APIView):
if owner.is_anonymous: if owner.is_anonymous:
owner = None owner = None
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer.save()
result = serializers.RSFormMetaSerializer(schema) result = s.LibraryItemSerializer(schema.item)
return Response(status=201, data=result.data) return Response(status=201, data=result.data)
@ -253,25 +274,27 @@ 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.RSFormMetaSerializer(data=request.data) serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = models.RSForm.objects.create( schema = m.RSForm.create(
title=serializer.validated_data['title'], title=serializer.validated_data['title'],
owner=owner, owner=owner,
alias=serializer.validated_data.get('alias', ''), alias=serializer.validated_data.get('alias', ''),
comment=serializer.validated_data.get('comment', ''), comment=serializer.validated_data.get('comment', ''),
is_common=serializer.validated_data.get('is_common', False), is_common=serializer.validated_data.get('is_common', False),
is_canonical=serializer.validated_data.get('is_canonical', False),
) )
else: else:
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
_prepare_rsform_data(data, request, owner) _prepare_rsform_data(data, request, owner)
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer.save()
result = serializers.RSFormMetaSerializer(schema) result = s.LibraryItemSerializer(schema.item)
return Response(status=201, data=result.data) return Response(status=201, data=result.data)
def _prepare_rsform_data(data: dict, request, owner: models.User): def _prepare_rsform_data(data: dict, request, owner: m.User):
data['owner'] = owner
if 'title' in request.data and request.data['title'] != '': if 'title' in request.data and request.data['title'] != '':
data['title'] = request.data['title'] data['title'] = request.data['title']
if data['title'] == '': if data['title'] == '':
@ -280,16 +303,21 @@ def _prepare_rsform_data(data: dict, request, owner: models.User):
data['alias'] = request.data['alias'] data['alias'] = request.data['alias']
if 'comment' in request.data and request.data['comment'] != '': if 'comment' in request.data and request.data['comment'] != '':
data['comment'] = request.data['comment'] data['comment'] = request.data['comment']
is_common = True is_common = True
if 'is_common' in request.data: if 'is_common' in request.data:
is_common = request.data['is_common'] == 'true' is_common = request.data['is_common'] == 'true'
data['is_common'] = is_common data['is_common'] = is_common
data['owner'] = owner
is_canonical = False
if 'is_canonical' in request.data:
is_canonical = request.data['is_canonical'] == 'true'
data['is_canonical'] = is_canonical
@api_view(['POST']) @api_view(['POST'])
def parse_expression(request): def parse_expression(request):
''' Endpoint: Parse RS expression. ''' ''' Endpoint: Parse RS expression. '''
serializer = serializers.ExpressionSerializer(data=request.data) serializer = s.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.parse_expression(expression) result = pyconcept.parse_expression(expression)
@ -299,7 +327,7 @@ def parse_expression(request):
@api_view(['POST']) @api_view(['POST'])
def convert_to_ascii(request): def convert_to_ascii(request):
''' Endpoint: Convert expression to ASCII syntax. ''' ''' Endpoint: Convert expression to ASCII syntax. '''
serializer = serializers.ExpressionSerializer(data=request.data) serializer = s.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.convert_to_ascii(expression) result = pyconcept.convert_to_ascii(expression)
@ -309,7 +337,7 @@ def convert_to_ascii(request):
@api_view(['POST']) @api_view(['POST'])
def convert_to_math(request): def convert_to_math(request):
''' Endpoint: Convert expression to MATH syntax. ''' ''' Endpoint: Convert expression to MATH syntax. '''
serializer = serializers.ExpressionSerializer(data=request.data) serializer = s.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.convert_to_math(expression) result = pyconcept.convert_to_math(expression)

File diff suppressed because it is too large Load Diff

View File

@ -13,18 +13,12 @@ def load_initial_schemas(apps, schema_editor):
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))
data['is_common'] = True data['is_common'] = True
data['is_canonical'] = True
serializer = RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer = RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
def load_initial_users(apps, schema_editor):
for n in range(1, 10, 1):
User.objects.create_user(
f'TestUser{n}', f'usermail{n}@gmail.com', '1234'
)
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
@ -35,5 +29,4 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.RunPython(load_initial_schemas), migrations.RunPython(load_initial_schemas),
migrations.RunPython(load_initial_users),
] ]

View File

@ -1,19 +1,19 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { DataCallback, deleteRSForm, getLibrary, postCloneRSForm, postNewRSForm } from '../utils/backendAPI'; import { DataCallback, deleteLibraryItem, getLibrary, postCloneLibraryItem, postNewRSForm } from '../utils/backendAPI';
import { ILibraryFilter, IRSFormCreateData, IRSFormData, IRSFormMeta, matchRSFormMeta } from '../utils/models'; import { ILibraryFilter, ILibraryItem, IRSFormCreateData, IRSFormData, matchLibraryItem } from '../utils/models';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
interface ILibraryContext { interface ILibraryContext {
items: IRSFormMeta[] items: ILibraryItem[]
loading: boolean loading: boolean
processing: boolean processing: boolean
error: ErrorInfo error: ErrorInfo
setError: (error: ErrorInfo) => void setError: (error: ErrorInfo) => void
filter: (params: ILibraryFilter) => IRSFormMeta[] filter: (params: ILibraryFilter) => ILibraryItem[]
createSchema: (data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => void createSchema: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void
cloneSchema: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void cloneSchema: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void
destroySchema: (target: number, callback?: () => void) => void destroySchema: (target: number, callback?: () => void) => void
} }
@ -34,7 +34,7 @@ interface LibraryStateProps {
} }
export const LibraryState = ({ children }: LibraryStateProps) => { export const LibraryState = ({ children }: LibraryStateProps) => {
const [ items, setItems ] = useState<IRSFormMeta[]>([]) const [ items, setItems ] = useState<ILibraryItem[]>([])
const [ loading, setLoading ] = useState(false); const [ loading, setLoading ] = useState(false);
const [ processing, setProcessing ] = useState(false); const [ processing, setProcessing ] = useState(false);
const [ error, setError ] = useState<ErrorInfo>(undefined); const [ error, setError ] = useState<ErrorInfo>(undefined);
@ -44,13 +44,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
(params: ILibraryFilter) => { (params: ILibraryFilter) => {
let result = items; let result = items;
if (params.ownedBy) { if (params.ownedBy) {
result = result.filter(schema => schema.owner === params.ownedBy); result = result.filter(item => item.owner === params.ownedBy);
} }
if (params.is_common !== undefined) { if (params.is_common !== undefined) {
result = result.filter(schema => schema.is_common === params.is_common); result = result.filter(item => item.is_common === params.is_common);
} }
if (params.queryMeta) { if (params.queryMeta) {
result = result.filter(schema => matchRSFormMeta(params.queryMeta!, schema)); result = result.filter(item => matchLibraryItem(params.queryMeta!, item));
} }
return result; return result;
}, [items]); }, [items]);
@ -75,13 +75,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
}, [reload, user]); }, [reload, user]);
const createSchema = useCallback( const createSchema = useCallback(
(data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => { (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => {
setError(undefined); setError(undefined);
postNewRSForm({ postNewRSForm({
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error); }, onError: error => setError(error),
onSuccess: newSchema => { onSuccess: newSchema => {
reload(); reload();
if (callback) callback(newSchema); if (callback) callback(newSchema);
@ -92,7 +92,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
const destroySchema = useCallback( const destroySchema = useCallback(
(target: number, callback?: () => void) => { (target: number, callback?: () => void) => {
setError(undefined) setError(undefined)
deleteRSForm(String(target), { deleteLibraryItem(String(target), {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
@ -108,7 +108,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
return; return;
} }
setError(undefined) setError(undefined)
postCloneRSForm(String(target), { postCloneLibraryItem(String(target), {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,

View File

@ -6,14 +6,14 @@ import { useRSFormDetails } from '../hooks/useRSFormDetails'
import { import {
type DataCallback, getTRSFile, type DataCallback, getTRSFile,
patchConstituenta, patchDeleteConstituenta, patchConstituenta, patchDeleteConstituenta,
patchLibraryItem,
patchMoveConstituenta, patchRenameConstituenta, patchMoveConstituenta, patchRenameConstituenta,
patchResetAliases, patchRSForm, patchResetAliases, patchUploadTRS, postClaimLibraryItem, postNewConstituenta
patchUploadTRS, postClaimRSForm, postNewConstituenta
} from '../utils/backendAPI' } from '../utils/backendAPI'
import { import {
IConstituentaList, IConstituentaMeta, ICstCreateData, IConstituentaList, IConstituentaMeta, ICstCreateData,
ICstMovetoData, ICstRenameData, ICstUpdateData, IRSForm, ICstMovetoData, ICstRenameData, ICstUpdateData, ILibraryItem,
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData ILibraryUpdateData, IRSForm, IRSFormUploadData
} from '../utils/models' } from '../utils/models'
import { useAuth } from './AuthContext' import { useAuth } from './AuthContext'
@ -35,8 +35,8 @@ interface IRSFormContext {
toggleReadonly: () => void toggleReadonly: () => void
toggleTracking: () => void toggleTracking: () => void
update: (data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => void update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void
claim: (callback?: DataCallback<IRSFormMeta>) => void claim: (callback?: DataCallback<ILibraryItem>) => void
download: (callback: DataCallback<Blob>) => void download: (callback: DataCallback<Blob>) => void
upload: (data: IRSFormUploadData, callback: () => void) => void upload: (data: IRSFormUploadData, callback: () => void) => void
@ -73,33 +73,41 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const [ isForceAdmin, setIsForceAdmin ] = useState(false); const [ isForceAdmin, setIsForceAdmin ] = useState(false);
const [ isReadonly, setIsReadonly ] = useState(false); const [ isReadonly, setIsReadonly ] = useState(false);
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner]); const isOwned = useMemo(
const isClaimable = useMemo(() => user?.id !== schema?.owner || false, [user, schema?.owner]); () => {
return user?.id === schema?.owner || false;
}, [user, schema?.owner]);
const isClaimable = useMemo(
() => {
return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false;
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical]);
const isEditable = useMemo( const isEditable = useMemo(
() => { () => {
return ( return (
!loading && !processing && !isReadonly && !loading && !processing && !isReadonly &&
((isOwned || (isForceAdmin && user?.is_staff)) ?? false) ((isOwned || (isForceAdmin && user?.is_staff)) ?? false)
) );
}, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading, processing]) }, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading, processing]);
const isTracking = useMemo( const isTracking = useMemo(
() => { () => {
return true return true;
}, []); }, []);
const toggleTracking = useCallback( const toggleTracking = useCallback(
() => { () => {
toast.info('Отслеживание в разработке...') toast.info('Отслеживание в разработке...')
}, []); }, []);
const update = useCallback( const update = useCallback(
(data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => { (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!schema) { if (!schema) {
return; return;
} }
setError(undefined) setError(undefined)
patchRSForm(schemaID, { patchLibraryItem(schemaID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -130,12 +138,12 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
}, [schemaID, setError, setSchema, schema]); }, [schemaID, setError, setSchema, schema]);
const claim = useCallback( const claim = useCallback(
(callback?: DataCallback<IRSFormMeta>) => { (callback?: DataCallback<ILibraryItem>) => {
if (!schema || !user) { if (!schema || !user) {
return; return;
} }
setError(undefined) setError(undefined)
postClaimRSForm(schemaID, { postClaimLibraryItem(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),

View File

@ -11,7 +11,7 @@ import TextArea from '../components/Common/TextArea';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import RequireAuth from '../components/RequireAuth'; import RequireAuth from '../components/RequireAuth';
import { useLibrary } from '../context/LibraryContext'; import { useLibrary } from '../context/LibraryContext';
import { IRSFormCreateData } from '../utils/models'; import { IRSFormCreateData, LibraryItemType } from '../utils/models';
function CreateRSFormPage() { function CreateRSFormPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -41,10 +41,12 @@ function CreateRSFormPage() {
return; return;
} }
const data: IRSFormCreateData = { const data: IRSFormCreateData = {
item_type: LibraryItemType.RSFORM,
title: title, title: title,
alias: alias, alias: alias,
comment: comment, comment: comment,
is_common: common, is_common: common,
is_canonical: false,
file: file, file: file,
fileName: file?.name fileName: file?.name
}; };

View File

@ -6,19 +6,19 @@ import ConceptDataTable from '../../components/Common/ConceptDataTable';
import TextURL from '../../components/Common/TextURL'; import TextURL from '../../components/Common/TextURL';
import { useNavSearch } from '../../context/NavSearchContext'; import { useNavSearch } from '../../context/NavSearchContext';
import { useUsers } from '../../context/UsersContext'; import { useUsers } from '../../context/UsersContext';
import { IRSFormMeta } from '../../utils/models' import { ILibraryItem } from '../../utils/models'
interface ViewLibraryProps { interface ViewLibraryProps {
schemas: IRSFormMeta[] items: ILibraryItem[]
} }
function ViewLibrary({ schemas }: ViewLibraryProps) { function ViewLibrary({ items }: ViewLibraryProps) {
const { resetQuery: cleanQuery } = useNavSearch(); const { resetQuery: cleanQuery } = useNavSearch();
const navigate = useNavigate(); const navigate = useNavigate();
const intl = useIntl(); const intl = useIntl();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const openRSForm = (schema: IRSFormMeta) => navigate(`/rsforms/${schema.id}`); const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`);
const columns = useMemo(() => const columns = useMemo(() =>
[ [
@ -26,7 +26,17 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
name: 'Шифр', name: 'Шифр',
id: 'alias', id: 'alias',
maxWidth: '140px', maxWidth: '140px',
selector: (schema: IRSFormMeta) => schema.alias, selector: (item: ILibraryItem) => item.alias,
sortable: true,
reorder: true
},
{
name: 'Статусы',
id: 'status',
maxWidth: '50px',
selector: (item: ILibraryItem) => {
return `${item.is_canonical ? 'C': ''}${item.is_common ? 'S': ''}`
},
sortable: true, sortable: true,
reorder: true reorder: true
}, },
@ -34,16 +44,16 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
name: 'Название', name: 'Название',
id: 'title', id: 'title',
minWidth: '50%', minWidth: '50%',
selector: (schema: IRSFormMeta) => schema.title, selector: (item: ILibraryItem) => item.title,
sortable: true, sortable: true,
reorder: true reorder: true
}, },
{ {
name: 'Владелец', name: 'Владелец',
id: 'owner', id: 'owner',
selector: (schema: IRSFormMeta) => schema.owner ?? 0, selector: (item: ILibraryItem) => item.owner ?? 0,
format: (schema: IRSFormMeta) => { format: (item: ILibraryItem) => {
return getUserLabel(schema.owner); return getUserLabel(item.owner);
}, },
sortable: true, sortable: true,
reorder: true reorder: true
@ -51,8 +61,8 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
{ {
name: 'Обновлена', name: 'Обновлена',
id: 'time_update', id: 'time_update',
selector: (schema: IRSFormMeta) => schema.time_update, selector: (item: ILibraryItem) => item.time_update,
format: (schema: IRSFormMeta) => new Date(schema.time_update).toLocaleString(intl.locale), format: (item: ILibraryItem) => new Date(item.time_update).toLocaleString(intl.locale),
sortable: true, sortable: true,
reorder: true reorder: true
} }
@ -61,7 +71,7 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
return ( return (
<ConceptDataTable <ConceptDataTable
columns={columns} columns={columns}
data={schemas} data={items}
defaultSortFieldId='time_update' defaultSortFieldId='time_update'
defaultSortAsc={false} defaultSortAsc={false}
striped striped

View File

@ -6,7 +6,7 @@ import { Loader } from '../../components/Common/Loader'
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useNavSearch } from '../../context/NavSearchContext'; import { useNavSearch } from '../../context/NavSearchContext';
import { ILibraryFilter, IRSFormMeta } from '../../utils/models'; import { ILibraryFilter, ILibraryItem } from '../../utils/models';
import ViewLibrary from './ViewLibrary'; import ViewLibrary from './ViewLibrary';
function LibraryPage() { function LibraryPage() {
@ -16,7 +16,7 @@ function LibraryPage() {
const library = useLibrary(); const library = useLibrary();
const [ filterParams, setFilterParams ] = useState<ILibraryFilter>({}); const [ filterParams, setFilterParams ] = useState<ILibraryFilter>({});
const [ items, setItems ] = useState<IRSFormMeta[]>([]); const [ items, setItems ] = useState<ILibraryItem[]>([]);
useLayoutEffect(() => { useLayoutEffect(() => {
const filterType = new URLSearchParams(search).get('filter'); const filterType = new URLSearchParams(search).get('filter');
@ -43,7 +43,7 @@ function LibraryPage() {
<div className='w-full'> <div className='w-full'>
{ library.loading && <Loader /> } { library.loading && <Loader /> }
{ library.error && <BackendError error={library.error} />} { library.error && <BackendError error={library.error} />}
{ !library.loading && library.items && <ViewLibrary schemas={items} /> } { !library.loading && library.items && <ViewLibrary items={items} /> }
</div> </div>
); );
} }

View File

@ -21,16 +21,18 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
const [canonical, setCanonical] = useState(false);
const { cloneSchema } = useLibrary(); const { cloneSchema } = useLibrary();
const { schema } = useRSForm(); const { schema } = useRSForm();
useEffect(() => { useEffect(() => {
if (schema) { if (schema) {
setTitle(getCloneTitle(schema)) setTitle(getCloneTitle(schema));
setAlias(schema.alias) setAlias(schema.alias);
setComment(schema.comment) setComment(schema.comment);
setCommon(schema.is_common) setCommon(schema.is_common);
setCanonical(false);
} }
}, [schema, schema?.title, schema?.alias, schema?.comment, schema?.is_common]); }, [schema, schema?.title, schema?.alias, schema?.comment, schema?.is_common]);
@ -39,10 +41,12 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
return; return;
} }
const data: IRSFormCreateData = { const data: IRSFormCreateData = {
item_type: schema.item_type,
title: title, title: title,
alias: alias, alias: alias,
comment: comment, comment: comment,
is_common: common is_common: common,
is_canonical: canonical
}; };
cloneSchema(schema.id, data, newSchema => { cloneSchema(schema.id, data, newSchema => {
toast.success(`Схема создана: ${newSchema.alias}`); toast.success(`Схема создана: ${newSchema.alias}`);

View File

@ -1,4 +1,4 @@
import { useCallback, useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -13,19 +13,21 @@ import { CrownIcon, DownloadIcon, DumpBinIcon, HelpIcon, SaveIcon, ShareIcon } f
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useUsers } from '../../context/UsersContext'; import { useUsers } from '../../context/UsersContext';
import { IRSFormCreateData } from '../../utils/models'; import { IRSFormCreateData, LibraryItemType } from '../../utils/models';
import { claimOwnershipProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
interface EditorRSFormProps { interface EditorRSFormProps {
onDestroy: () => void onDestroy: () => void
onClaim: () => void
onShare: () => void
onDownload: () => void
} }
function EditorRSForm({ onDestroy }: EditorRSFormProps) { function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormProps) {
const intl = useIntl(); const intl = useIntl();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const { const {
schema, update, download, schema, update, isForceAdmin,
isEditable, isOwned, isClaimable, processing, claim isEditable, isOwned, isClaimable, processing
} = useRSForm(); } = useRSForm();
const { user } = useAuth(); const { user } = useAuth();
@ -33,6 +35,7 @@ function EditorRSForm({ onDestroy }: EditorRSFormProps) {
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
const [canonical, setCanonical] = useState(false);
const [isModified, setIsModified] = useState(true); const [isModified, setIsModified] = useState(true);
@ -45,10 +48,12 @@ function EditorRSForm({ onDestroy }: EditorRSFormProps) {
schema.title !== title || schema.title !== title ||
schema.alias !== alias || schema.alias !== alias ||
schema.comment !== comment || schema.comment !== comment ||
schema.is_common !== common schema.is_common !== common ||
schema.is_canonical !== canonical
); );
}, [schema, schema?.title, schema?.alias, schema?.comment, schema?.is_common, }, [schema, schema?.title, schema?.alias, schema?.comment,
title, alias, comment, common]); schema?.is_common, schema?.is_canonical,
title, alias, comment, common, canonical]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (schema) { if (schema) {
@ -56,25 +61,23 @@ function EditorRSForm({ onDestroy }: EditorRSFormProps) {
setAlias(schema.alias); setAlias(schema.alias);
setComment(schema.comment); setComment(schema.comment);
setCommon(schema.is_common); setCommon(schema.is_common);
setCanonical(schema.is_canonical);
} }
}, [schema]); }, [schema]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const data: IRSFormCreateData = { const data: IRSFormCreateData = {
item_type: LibraryItemType.RSFORM,
title: title, title: title,
alias: alias, alias: alias,
comment: comment, comment: comment,
is_common: common is_common: common,
is_canonical: canonical
}; };
update(data, () => toast.success('Изменения сохранены')); update(data, () => toast.success('Изменения сохранены'));
}; };
const handleDownload = useCallback(() => {
const fileName = (schema?.alias ?? 'Schema') + '.trs';
downloadRSFormProc(download, fileName);
}, [download, schema?.alias]);
return ( return (
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'> <form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
<div className='relative w-full'> <div className='relative w-full'>
@ -82,18 +85,18 @@ function EditorRSForm({ onDestroy }: EditorRSFormProps) {
<MiniButton <MiniButton
tooltip='Поделиться схемой' tooltip='Поделиться схемой'
icon={<ShareIcon size={5} color='text-primary'/>} icon={<ShareIcon size={5} color='text-primary'/>}
onClick={shareCurrentURLProc} onClick={onShare}
/> />
<MiniButton <MiniButton
tooltip='Скачать TRS файл' tooltip='Скачать TRS файл'
icon={<DownloadIcon size={5} color='text-primary'/>} icon={<DownloadIcon size={5} color='text-primary'/>}
onClick={handleDownload} onClick={onDownload}
/> />
<MiniButton <MiniButton
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' } tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
icon={<CrownIcon size={5} color={isOwned ? '' : 'text-green'}/>} icon={<CrownIcon size={5} color={isOwned ? '' : 'text-green'}/>}
disabled={!isClaimable || !user} disabled={!isClaimable || !user}
onClick={() => { claimOwnershipProc(claim); }} onClick={onClaim}
/> />
<MiniButton <MiniButton
tooltip='Удалить схему' tooltip='Удалить схему'
@ -113,25 +116,35 @@ function EditorRSForm({ onDestroy }: EditorRSFormProps) {
required required
value={title} value={title}
disabled={!isEditable} disabled={!isEditable}
onChange={event => { setTitle(event.target.value); }} onChange={event => setTitle(event.target.value)}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput id='alias' label='Сокращение' type='text'
required required
value={alias} value={alias}
disabled={!isEditable} disabled={!isEditable}
widthClass='max-w-sm' widthClass='max-w-sm'
onChange={event => { setAlias(event.target.value); }} onChange={event => setAlias(event.target.value)}
/> />
<TextArea id='comment' label='Комментарий' <TextArea id='comment' label='Комментарий'
value={comment} value={comment}
disabled={!isEditable} disabled={!isEditable}
onChange={event => { setComment(event.target.value); }} onChange={event => setComment(event.target.value)}
/>
<Checkbox id='common' label='Общедоступная схема'
value={common}
disabled={!isEditable}
onChange={event => { setCommon(event.target.checked); }}
/> />
<div className='flex justify-between whitespace-nowrap'>
<div></div>
<Checkbox id='common' label='Общедоступная схема'
value={common}
disabled={!isEditable}
onChange={event => setCommon(event.target.checked)}
/>
<Checkbox id='canonical' label='Библиотечная схема'
widthClass='w-fit'
value={canonical}
tooltip='Только администраторы могут присваивать схемам библиотечный статус'
disabled={!isEditable || !isForceAdmin}
onChange={event => setCanonical(event.target.checked)}
/>
</div>
<div className='flex items-center justify-between gap-1 py-2 mt-2'> <div className='flex items-center justify-between gap-1 py-2 mt-2'>
<SubmitButton <SubmitButton

View File

@ -1,3 +1,4 @@
import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
@ -35,7 +36,7 @@ function RSTabs() {
const navigate = useNavigate(); const navigate = useNavigate();
const search = useLocation().search; const search = useLocation().search;
const { const {
error, schema, loading, error, schema, loading, claim, download,
cstCreate, cstDelete, cstRename cstCreate, cstDelete, cstRename
} = useRSForm(); } = useRSForm();
const { destroySchema } = useLibrary(); const { destroySchema } = useLibrary();
@ -195,6 +196,35 @@ function RSTabs() {
}); });
}, [schema, destroySchema, navigate]); }, [schema, destroySchema, navigate]);
const onClaimSchema = useCallback(
() => {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return;
}
claim(() => toast.success('Вы стали владельцем схемы'));
}, [claim]);
const onShareSchema = useCallback(
() => {
const url = window.location.href + '&share';
navigator.clipboard.writeText(url)
.then(() => toast.success(`Ссылка скопирована: ${url}`))
.catch(console.error);
}, []);
const onDownloadSchema = useCallback(
() => {
const fileName = (schema?.alias ?? 'Schema') + '.trs';
download(
(data) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}, [schema?.alias, download]);
return ( return (
<div className='w-full'> <div className='w-full'>
{ loading && <Loader /> } { loading && <Loader /> }
@ -240,7 +270,10 @@ function RSTabs() {
> >
<TabList className='flex items-start pl-2 select-none w-fit clr-bg-pop'> <TabList className='flex items-start pl-2 select-none w-fit clr-bg-pop'>
<RSTabsMenu <RSTabsMenu
onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
showCloneDialog={() => setShowClone(true)} showCloneDialog={() => setShowClone(true)}
showUploadDialog={() => setShowUpload(true)} showUploadDialog={() => setShowUpload(true)}
/> />
@ -255,7 +288,10 @@ function RSTabs() {
<TabPanel className='flex items-start w-full gap-2 px-2'> <TabPanel className='flex items-start w-full gap-2 px-2'>
<EditorRSForm <EditorRSForm
onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
/> />
{schema.stats && <RSFormStats stats={schema.stats}/>} {schema.stats && <RSFormStats stats={schema.stats}/>}
</TabPanel> </TabPanel>

View File

@ -1,4 +1,3 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
@ -9,55 +8,57 @@ import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, M
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useDropdown from '../../hooks/useDropdown'; import useDropdown from '../../hooks/useDropdown';
import { claimOwnershipProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
interface RSTabsMenuProps { interface RSTabsMenuProps {
showUploadDialog: () => void showUploadDialog: () => void
showCloneDialog: () => void showCloneDialog: () => void
onDestroy: () => void onDestroy: () => void
onClaim: () => void
onShare: () => void
onDownload: () => void
} }
function RSTabsMenu({showUploadDialog, showCloneDialog, onDestroy}: RSTabsMenuProps) { function RSTabsMenu({
showUploadDialog, showCloneDialog,
onDestroy, onShare, onDownload, onClaim
}: RSTabsMenuProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const { const {
schema,
isOwned, isEditable, isTracking, isReadonly: readonly, isForceAdmin: forceAdmin, isOwned, isEditable, isTracking, isReadonly: readonly, isForceAdmin: forceAdmin,
toggleTracking, toggleForceAdmin, toggleReadonly, toggleTracking, toggleForceAdmin, toggleReadonly
claim, download
} = useRSForm(); } = useRSForm();
const schemaMenu = useDropdown(); const schemaMenu = useDropdown();
const editMenu = useDropdown(); const editMenu = useDropdown();
const handleClaimOwner = useCallback(() => { function handleClaimOwner() {
editMenu.hide(); editMenu.hide();
claimOwnershipProc(claim) onClaim();
}, [claim, editMenu]); }
const handleDelete = useCallback(() => { function handleDelete() {
schemaMenu.hide(); schemaMenu.hide();
onDestroy(); onDestroy();
}, [onDestroy, schemaMenu]); }
const handleDownload = useCallback(() => { function handleDownload () {
schemaMenu.hide(); schemaMenu.hide();
const fileName = (schema?.alias ?? 'Schema') + '.trs'; onDownload();
downloadRSFormProc(download, fileName); }
}, [schemaMenu, download, schema?.alias]);
const handleUpload = useCallback(() => { function handleUpload() {
schemaMenu.hide(); schemaMenu.hide();
showUploadDialog(); showUploadDialog();
}, [schemaMenu, showUploadDialog]); }
const handleClone = useCallback(() => { function handleClone() {
schemaMenu.hide(); schemaMenu.hide();
showCloneDialog(); showCloneDialog();
}, [schemaMenu, showCloneDialog]); }
function handleShare() { function handleShare() {
schemaMenu.hide(); schemaMenu.hide();
shareCurrentURLProc(); onShare();
} }
function handleCreateNew() { function handleCreateNew() {

View File

@ -6,9 +6,8 @@ import { config } from './constants'
import { import {
IConstituentaList, IConstituentaMeta, IConstituentaList, IConstituentaMeta,
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData, ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData,
ICurrentUser, IExpressionParse, IReferenceData, IRefsText, IRSExpression, ICurrentUser, IExpressionParse, ILibraryItem, ILibraryUpdateData, IReferenceData, IRefsText,
IRSFormCreateData, IRSFormData, IRSExpression, IRSFormCreateData, IRSFormData, IRSFormUploadData, IUserInfo,
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo,
IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData, IUserUpdatePassword IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData, IUserUpdatePassword
} from './models' } from './models'
@ -116,15 +115,15 @@ export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
}); });
} }
export function getLibrary(request: FrontPull<IRSFormMeta[]>) { export function getLibrary(request: FrontPull<ILibraryItem[]>) {
AxiosGet({ AxiosGet({
title: 'Available RSForms (Library) list', title: 'Available RSForms (Library) list',
endpoint: '/api/library/', endpoint: '/api/library/active/',
request: request request: request
}); });
} }
export function postNewRSForm(request: FrontExchange<IRSFormCreateData, IRSFormMeta>) { export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibraryItem>) {
AxiosPost({ AxiosPost({
title: 'New RSForm', title: 'New RSForm',
endpoint: '/api/rsforms/create-detailed/', endpoint: '/api/rsforms/create-detailed/',
@ -137,10 +136,10 @@ export function postNewRSForm(request: FrontExchange<IRSFormCreateData, IRSFormM
}); });
} }
export function postCloneRSForm(schema: string, request: FrontExchange<IRSFormCreateData, IRSFormData>) { export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCreateData, IRSFormData>) {
AxiosPost({ AxiosPost({
title: 'clone RSForm', title: 'clone RSForm',
endpoint: `/api/rsforms/${schema}/clone/`, endpoint: `/api/library/${target}/clone/`,
request: request request: request
}); });
} }
@ -153,26 +152,26 @@ export function getRSFormDetails(target: string, request: FrontPull<IRSFormData>
}); });
} }
export function patchRSForm(target: string, request: FrontExchange<IRSFormUpdateData, IRSFormMeta>) { export function patchLibraryItem(target: string, request: FrontExchange<ILibraryUpdateData, ILibraryItem>) {
AxiosPatch({ AxiosPatch({
title: `RSForm id=${target}`, title: `RSForm id=${target}`,
endpoint: `/api/rsforms/${target}/`, endpoint: `/api/library/${target}/`,
request: request request: request
}); });
} }
export function deleteRSForm(target: string, request: FrontAction) { export function deleteLibraryItem(target: string, request: FrontAction) {
AxiosDelete({ AxiosDelete({
title: `RSForm id=${target}`, title: `RSForm id=${target}`,
endpoint: `/api/rsforms/${target}/`, endpoint: `/api/library/${target}/`,
request: request request: request
}); });
} }
export function postClaimRSForm(target: string, request: FrontPull<IRSFormMeta>) { export function postClaimLibraryItem(target: string, request: FrontPull<ILibraryItem>) {
AxiosPost({ AxiosPost({
title: `Claim on RSForm id=${target}`, title: `Claim on RSForm id=${target}`,
endpoint: `/api/rsforms/${target}/claim/`, endpoint: `/api/library/${target}/claim/`,
request: request request: request
}); });
} }

View File

@ -214,6 +214,29 @@ export interface ICstCreatedResponse {
schema: IRSFormData schema: IRSFormData
} }
// ========== LibraryItem ============
export enum LibraryItemType {
RSFORM = 'rsform',
OPERATIONS_SCHEMA = 'oss'
}
export interface ILibraryItem {
id: number
item_type: LibraryItemType
title: string
alias: string
comment: string
is_common: boolean
is_canonical: boolean
time_create: string
time_update: string
owner: number | null
}
export interface ILibraryUpdateData
extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {}
// ========== RSForm ============ // ========== RSForm ============
export interface IRSFormStats { export interface IRSFormStats {
count_all: number count_all: number
@ -233,28 +256,17 @@ export interface IRSFormStats {
count_theorem: number count_theorem: number
} }
export interface IRSForm { export interface IRSForm
id: number extends ILibraryItem {
title: string
alias: string
comment: string
is_common: boolean
time_create: string
time_update: string
owner: number | null
items: IConstituenta[] items: IConstituenta[]
stats: IRSFormStats stats: IRSFormStats
graph: Graph graph: Graph
} }
export interface IRSFormData extends Omit<IRSForm, 'stats' | 'graph'> {} export interface IRSFormData extends Omit<IRSForm, 'stats' | 'graph'> {}
export interface IRSFormMeta extends Omit<IRSForm, 'items' | 'stats' | 'graph'> {}
export interface IRSFormUpdateData
extends Omit<IRSFormMeta, 'time_create' | 'time_update' | 'id' | 'owner'> {}
export interface IRSFormCreateData export interface IRSFormCreateData
extends IRSFormUpdateData { extends ILibraryUpdateData {
file?: File file?: File
fileName?: string fileName?: string
} }
@ -454,7 +466,7 @@ export function matchConstituenta(query: string, target: IConstituenta, mode: Cs
return false; return false;
} }
export function matchRSFormMeta(query: string, target: IRSFormMeta) { export function matchLibraryItem(query: string, target: ILibraryItem): boolean {
const queryI = query.toUpperCase(); const queryI = query.toUpperCase();
if (target.alias.toUpperCase().match(queryI)) { if (target.alias.toUpperCase().match(queryI)) {
return true; return true;

View File

@ -1,34 +0,0 @@
import fileDownload from 'js-file-download';
import { toast } from 'react-toastify';
import { type DataCallback } from './backendAPI';
import { IRSFormMeta } from './models';
export function shareCurrentURLProc() {
const url = window.location.href + '&share';
navigator.clipboard.writeText(url)
.then(() => toast.success(`Ссылка скопирована: ${url}`))
.catch(console.error);
}
export function claimOwnershipProc(
claim: (callback: DataCallback<IRSFormMeta>) => void
) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return;
}
claim(() => toast.success('Вы стали владельцем схемы'));
}
export function downloadRSFormProc(
download: (callback: DataCallback<Blob>) => void,
fileName: string
) {
download((data) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}