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

View File

@ -8,9 +8,9 @@ class ConstituentaAdmin(admin.ModelAdmin):
''' Admin model: Constituenta. '''
class RSFormAdmin(admin.ModelAdmin):
''' Admin model: RSForm. '''
class Librarydmin(admin.ModelAdmin):
''' Admin model: LibraryItem. '''
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
from django.conf import settings
@ -17,13 +17,15 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='RSForm',
name='LibraryItem',
fields=[
('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='Название')),
('alias', models.CharField(blank=True, max_length=255, verbose_name='Шифр')),
('comment', models.TextField(blank=True, 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_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='Владелец')),
@ -33,6 +35,18 @@ class Migration(migrations.Migration):
'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(
name='Constituenta',
fields=[
@ -47,7 +61,7 @@ class Migration(migrations.Migration):
('definition_formal', 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='Текстовое определние')),
('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={
'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]+)')
class LibraryItemType(TextChoices):
''' Type of library items '''
RSFORM = 'rsform'
OPERATIONS_SCHEMA = 'oss'
class CstType(TextChoices):
''' Type of constituenta '''
BASE = 'basic'
@ -67,8 +73,14 @@ def _get_type_prefix(cst_type: CstType) -> str:
return 'X'
class RSForm(Model):
''' RSForm is a math form of capturing conceptual schema '''
class LibraryItem(Model):
''' 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(
verbose_name='Владелец',
to=User,
@ -91,6 +103,10 @@ class RSForm(Model):
verbose_name='Общая',
default=False
)
is_canonical: BooleanField = BooleanField(
verbose_name='Каноничная',
default=False
)
time_create: DateTimeField = DateTimeField(
verbose_name='Дата создания',
auto_now_add=True
@ -105,9 +121,127 @@ class RSForm(Model):
verbose_name = 'Схема'
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']:
''' Get QuerySet containing all constituents of current RSForm '''
return Constituenta.objects.filter(schema=self)
''' Get QuerySet containing all constituents of current RSForm. '''
return Constituenta.objects.filter(schema=self.item)
def resolver(self) -> Resolver:
''' 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 '''
if position <= 0:
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:
cst.order += 1
Constituenta.objects.bulk_update(update_list, ['order'])
result = Constituenta.objects.create(
schema=self,
schema=self.item,
order=position,
alias=alias,
cst_type=insert_type
)
self.update_order()
self.save()
self.item.save()
result.refresh_from_db()
return result
@ -175,13 +309,13 @@ class RSForm(Model):
if self.constituents().exists():
position += self.constituents().count()
result = Constituenta.objects.create(
schema=self,
schema=self.item,
order=position,
alias=alias,
cst_type=insert_type
)
self.update_order()
self.save()
self.item.save()
result.refresh_from_db()
return result
@ -207,7 +341,7 @@ class RSForm(Model):
update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['order'])
self.update_order()
self.save()
self.item.save()
@transaction.atomic
def delete_cst(self, listCst):
@ -216,7 +350,7 @@ class RSForm(Model):
cst.delete()
self.update_order()
self.resolve_all_text()
self.save()
self.item.save()
@transaction.atomic
def create_cst(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta':
@ -326,12 +460,6 @@ class RSForm(Model):
else:
return self.insert_last(data['alias'], data['cst_type'])
def __str__(self) -> str:
return f'{self.title}'
def get_absolute_url(self):
return reverse('rsform-detail', kwargs={'pk': self.pk})
def _term_graph(self) -> Graph:
result = Graph()
cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order')
@ -355,83 +483,6 @@ class RSForm(Model):
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:
''' RSForm adapter for interacting with pyconcept module. '''
def __init__(self, instance: RSForm):
@ -456,14 +507,14 @@ class PyConceptAdapter:
def _complete_rsform_details(self, data: dict) -> dict:
result = deepcopy(data)
result['id'] = self.schema.pk
result['alias'] = self.schema.alias
result['title'] = self.schema.title
result['comment'] = self.schema.comment
result['time_update'] = self.schema.time_update
result['time_create'] = self.schema.time_create
result['is_common'] = self.schema.is_common
result['owner'] = (self.schema.owner.pk if self.schema.owner is not None else None)
result['id'] = self.schema.item.pk
result['alias'] = self.schema.item.alias
result['title'] = self.schema.item.title
result['comment'] = self.schema.item.comment
result['time_update'] = self.schema.item.time_update
result['time_create'] = self.schema.item.time_create
result['is_common'] = self.schema.item.is_common
result['owner'] = (self.schema.item.owner.pk if self.schema.item.owner is not None else None)
for cst_data in result['items']:
cst = Constituenta.objects.get(pk=cst_data['id'])
cst_data['convention'] = cst.convention

View File

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

View File

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

View File

@ -9,7 +9,7 @@ from rest_framework.exceptions import ErrorDetail
from cctext import ReferenceType
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 (
convert_to_ascii,
convert_to_math,
@ -17,28 +17,28 @@ from apps.rsform.views import (
)
def _response_contains(response, schema: RSForm) -> bool:
return any(x for x in response.data if x['id'] == schema.pk)
def _response_contains(response, item: LibraryItem) -> bool:
return any(x for x in response.data if x['id'] == item.pk)
class TestConstituentaAPI(APITestCase):
''' Testing constituenta view. '''
''' Testing Constituenta 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.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
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_forms=[{'text':'form1', 'tags':'sing,datv'}])
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')
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',
definition_raw='Test1', definition_resolved='Test2')
@ -102,6 +102,110 @@ class TestConstituentaAPI(APITestCase):
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):
''' Testing RSForm view. '''
def setUp(self):
@ -109,56 +213,34 @@ class TestRSFormViewset(APITestCase):
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.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.unowned = RSForm.create(title='Test2', alias='T2')
def test_create_anonymous(self):
self.client.logout()
data = json.dumps({'title': 'Title'})
response = self.client.post('/api/rsforms/', 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/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')
def test_list(self):
non_schema = LibraryItem.objects.create(
item_type=LibraryItemType.OPERATIONS_SCHEMA,
title='Test3'
)
response = self.client.get('/api/rsforms/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'New title')
self.assertEqual(response.data['alias'], self.rsform_owned.alias)
self.assertFalse(_response_contains(response, non_schema))
self.assertTrue(_response_contains(response, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item))
def test_update_unowned(self):
data = json.dumps({'id': self.rsform_unowned.id, 'title': 'New title'})
response = self.client.patch(f'/api/rsforms/{self.rsform_unowned.id}/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 403)
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])
response = self.client.get('/api/library/')
self.assertEqual(response.status_code, 200)
self.assertTrue(_response_contains(response, non_schema))
self.assertTrue(_response_contains(response, self.unowned.item))
self.assertTrue(_response_contains(response, self.owned.item))
def test_contents(self):
schema = RSForm.objects.create(title='Title1')
schema = RSForm.create(title='Title1')
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)
def test_details(self):
schema = RSForm.objects.create(title='Test')
schema = RSForm.create(title='Test')
x1 = schema.insert_at(1, 'X1', CstType.BASE)
x2 = schema.insert_at(2, 'X2', CstType.BASE)
x1.term_raw = 'человек'
@ -168,7 +250,7 @@ class TestRSFormViewset(APITestCase):
x1.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.data['title'], 'Test')
@ -182,10 +264,10 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['items'][1]['term']['resolved'], x2.term_resolved)
def test_check(self):
schema = RSForm.objects.create(title='Test')
schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE)
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.data['parseResult'], True)
self.assertEqual(response.data['syntax'], Syntax.MATH)
@ -194,12 +276,12 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['valueClass'], 'value')
def test_resolve(self):
schema = RSForm.objects.create(title='Test')
schema = RSForm.create(title='Test')
x1 = schema.insert_at(1, 'X1', CstType.BASE)
x1.term_resolved = 'синий слон'
x1.save()
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.data['input'], '@{1|редкий} @{X1|plur,datv}')
self.assertEqual(response.data['output'], 'редким синим слонам')
@ -231,9 +313,9 @@ class TestRSFormViewset(APITestCase):
self.assertTrue(response.data['title'] != '')
def test_export_trs(self):
schema = RSForm.objects.create(title='Test')
schema = RSForm.create(title='Test')
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.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
with io.BytesIO(response.content) as stream:
@ -241,29 +323,16 @@ class TestRSFormViewset(APITestCase):
self.assertIsNone(zipped_file.testzip())
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):
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')
self.assertEqual(response.status_code, 403)
schema = self.rsform_owned
Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2)
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
item = self.owned.item
Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
@ -271,7 +340,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x3.order, 3)
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')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X4')
@ -280,34 +349,34 @@ class TestRSFormViewset(APITestCase):
def test_rename_constituenta(self):
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_forms=[{'text':'form1', 'tags':'sing,datv'}])
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')
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',
definition_raw='Test1', definition_resolved='Test2')
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')
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')
self.assertEqual(response.status_code, 400)
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')
self.assertEqual(response.status_code, 400)
data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk})
schema = self.rsform_owned
d1 = Constituenta.objects.create(schema=schema, alias='D1', cst_type='term', order=4)
item = self.owned.item
d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4)
d1.term_raw = '@{X1|plur}'
d1.definition_formal = 'X1'
d1.save()
@ -315,7 +384,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(self.cst1.order, 1)
self.assertEqual(self.cst1.alias, 'X1')
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')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['new_cst']['alias'], 'D2')
@ -337,8 +406,8 @@ class TestRSFormViewset(APITestCase):
'definition_formal': '3',
'definition_raw': '4'
})
schema = self.rsform_owned
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
item = self.owned.item
response = self.client.post(f'/api/rsforms/{item.id}/cst-create/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
@ -351,66 +420,66 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.data['new_cst']['definition_resolved'], '4')
def test_delete_constituenta(self):
schema = self.rsform_owned
schema = self.owned
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')
self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2)
x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2)
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')
x2.refresh_from_db()
schema.refresh_from_db()
schema.item.refresh_from_db()
self.assertEqual(response.status_code, 202)
self.assertEqual(len(response.data['items']), 1)
self.assertEqual(schema.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2')
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}]})
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')
self.assertEqual(response.status_code, 400)
def test_move_constituenta(self):
schema = self.rsform_owned
item = self.owned.item
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')
self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2)
x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
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')
x1.refresh_from_db()
x2.refresh_from_db()
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(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})
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')
self.assertEqual(response.status_code, 400)
def test_reset_aliases(self):
schema = self.rsform_owned
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
item = self.owned.item
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
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)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=2)
d11 = Constituenta.objects.create(schema=schema, alias='D11', cst_type='term', order=3)
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=1)
x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=2)
d11 = Constituenta.objects.create(schema=item, alias='D11', cst_type='term', order=3)
response = self.client.patch(f'/api/rsforms/{item.id}/reset-aliases/')
x1.refresh_from_db()
x2.refresh_from_db()
d11.refresh_from_db()
@ -422,31 +491,31 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(d11.order, 3)
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)
def test_load_trs(self):
schema = self.rsform_owned
schema.title = 'Testt11'
schema.save()
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
schema = self.owned
schema.item.title = 'Testt11'
schema.item.save()
x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1)
work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False}
response = self.client.patch(f'/api/rsforms/{schema.id}/load-trs/', data=data, format='multipart')
schema.refresh_from_db()
response = self.client.patch(f'/api/rsforms/{schema.item.id}/load-trs/', data=data, format='multipart')
schema.item.refresh_from_db()
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(schema.constituents().count(), 25)
self.assertFalse(Constituenta.objects.all().filter(pk=x1.id).exists())
def test_clone(self):
schema = self.rsform_owned
schema.title = 'Testt11'
schema.save()
x1 = Constituenta.objects.create(schema=schema, alias='X12', cst_type='basic', order=1)
d1 = Constituenta.objects.create(schema=schema, alias='D2', cst_type='term', order=1)
item = self.owned.item
item.title = 'Testt11'
item.save()
x1 = Constituenta.objects.create(schema=item, alias='X12', cst_type='basic', order=1)
d1 = Constituenta.objects.create(schema=item, alias='D2', cst_type='term', order=1)
x1.term_raw = 'человек'
x1.term_resolved = 'человек'
d1.term_raw = '@{X12|plur}'
@ -455,7 +524,7 @@ class TestRSFormViewset(APITestCase):
d1.save()
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.data['title'], 'Title')
@ -536,29 +605,3 @@ class TestFunctionalViews(APITestCase):
response = parse_expression(request)
self.assertEqual(response.status_code, 400)
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 . import views
rsform_router = routers.SimpleRouter()
rsform_router.register(r'rsforms', views.RSFormViewSet)
library_router = routers.SimpleRouter()
library_router.register('library', views.LibraryViewSet)
library_router.register('rsforms', views.RSFormViewSet)
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('rsforms/import-trs/', views.TrsImportView.as_view()),
path('rsforms/create-detailed/', views.create_rsform),
path('func/parse-expression/', views.parse_expression),
path('func/to-ascii/', views.convert_to_ascii),
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
import re
from zipfile import ZipFile
from rest_framework.permissions import BasePermission
from rest_framework.permissions import BasePermission, IsAuthenticated
class ObjectOwnerOrAdmin(BasePermission):
@ -16,6 +16,14 @@ class ObjectOwnerOrAdmin(BasePermission):
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):
''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj):

View File

@ -1,5 +1,6 @@
''' REST API: RSForms for conceptual schemas. '''
import json
from typing import cast
from django.db import transaction
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
@ -10,28 +11,28 @@ from rest_framework.response import Response
from rest_framework.decorators import api_view
import pyconcept
from . import models
from . import serializers
from . import models as m
from . import serializers as s
from . import utils
class LibraryView(generics.ListAPIView):
class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of rsforms available for active user. '''
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.RSFormMetaSerializer
serializer_class = s.LibraryItemSerializer
def get_queryset(self):
user = self.request.user
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:
return models.RSForm.objects.filter(is_common=True)
return m.LibraryItem.objects.filter(is_common=True)
class ConstituentAPIView(generics.RetrieveUpdateAPIView):
''' Endpoint: Get / Update Constituenta. '''
queryset = models.Constituenta.objects.all()
serializer_class = serializers.ConstituentaSerializer
queryset = m.Constituenta.objects.all()
serializer_class = s.ConstituentaSerializer
def get_permissions(self):
result = super().get_permissions()
@ -41,21 +42,17 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView):
result.append(utils.SchemaOwnerOrAdmin())
return result
# pylint: disable=too-many-ancestors
class RSFormViewSet(viewsets.ModelViewSet):
''' Endpoint: RSForm operations. '''
queryset = models.RSForm.objects.all()
serializer_class = serializers.RSFormMetaSerializer
class LibraryViewSet(viewsets.ModelViewSet):
''' Endpoint: Library operations. '''
queryset = m.LibraryItem.objects.all()
serializer_class = s.LibraryItemSerializer
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_fields = ['owner', 'is_common']
ordering_fields = ('owner', 'title', 'time_update')
filterset_fields = ['item_type', 'owner', 'is_common', 'is_canonical']
ordering_fields = ('item_type', 'owner', 'title', 'time_update')
ordering = '-time_update'
def _get_schema(self) -> models.RSForm:
return self.get_object() # type: ignore
def perform_create(self, serializer):
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
return serializer.save(owner=self.request.user)
@ -63,11 +60,64 @@ class RSFormViewSet(viewsets.ModelViewSet):
return serializer.save()
def get_permissions(self):
if self.action in ['update', 'destroy', 'partial_update', 'load_trs',
'cst_create', 'cst_multidelete', 'reset_aliases', 'cst_rename']:
if self.action in ['update', 'destroy', 'partial_update']:
permission_classes = [utils.ObjectOwnerOrAdmin]
elif self.action in ['create', 'claim', 'clone']:
elif self.action in ['create', 'clone']:
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:
permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes]
@ -76,14 +126,14 @@ class RSFormViewSet(viewsets.ModelViewSet):
def cst_create(self, request, pk):
''' Create new constituenta. '''
schema = self._get_schema()
serializer = serializers.CstCreateSerializer(data=request.data)
serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
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={
'new_cst': serializers.ConstituentaSerializer(new_cst).data,
'schema': models.PyConceptAdapter(schema).full()
'new_cst': s.ConstituentaSerializer(new_cst).data,
'schema': m.PyConceptAdapter(schema).full()
})
response['Location'] = new_cst.get_absolute_url()
return response
@ -93,109 +143,80 @@ class RSFormViewSet(viewsets.ModelViewSet):
def cst_rename(self, request, pk):
''' Rename constituenta possibly changing type. '''
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)
old_alias = models.Constituenta.objects.get(pk=request.data['id']).alias
old_alias = m.Constituenta.objects.get(pk=request.data['id']).alias
serializer.save()
mapping = { old_alias: serializer.validated_data['alias'] }
schema.apply_mapping(mapping, change_aliases=False)
schema.update_order()
schema.refresh_from_db()
cst = models.Constituenta.objects.get(pk=serializer.validated_data['id'])
schema.item.refresh_from_db()
cst = m.Constituenta.objects.get(pk=serializer.validated_data['id'])
return Response(status=200, data={
'new_cst': serializers.ConstituentaSerializer(cst).data,
'schema': models.PyConceptAdapter(schema).full()
'new_cst': s.ConstituentaSerializer(cst).data,
'schema': m.PyConceptAdapter(schema).full()
})
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
def cst_multidelete(self, request, pk):
''' Endpoint: Delete multiple constituents. '''
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)
schema.delete_cst(serializer.validated_data['constituents'])
schema.refresh_from_db()
return Response(status=202, data=models.PyConceptAdapter(schema).full())
schema.item.refresh_from_db()
return Response(status=202, data=m.PyConceptAdapter(schema).full())
@action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request, pk):
''' Endpoint: Move multiple constituents. '''
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)
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
schema.refresh_from_db()
return Response(status=200, data=models.PyConceptAdapter(schema).full())
schema.item.refresh_from_db()
return Response(status=200, data=m.PyConceptAdapter(schema).full())
@action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request, pk):
''' Endpoint: Recreate all aliases based on order. '''
schema = self._get_schema()
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')
def load_trs(self, request, pk):
''' 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)
schema = self._get_schema()
load_metadata = serializer.validated_data['load_metadata']
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)
schema = serializer.save()
return Response(status=200, data=models.PyConceptAdapter(schema).full())
@action(detail=True, methods=['post'], url_path='clone')
def clone(self, request, pk):
''' Endpoint: Clone RSForm constituents and create new schema using new metadata. '''
serializer = serializers.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)
return Response(status=200, data=m.PyConceptAdapter(schema).full())
@action(detail=True, methods=['get'])
def contents(self, request, pk):
''' Endpoint: View schema db contents (including constituents). '''
schema = serializers.RSFormContentsSerializer(self._get_schema()).data
schema = s.RSFormSerializer(self._get_schema()).data
return Response(schema)
@action(detail=True, methods=['get'])
def details(self, request, pk):
''' Endpoint: Detailed schema view including statuses and parse. '''
schema = self._get_schema()
serializer = models.PyConceptAdapter(schema)
serializer = m.PyConceptAdapter(schema)
return Response(serializer.full())
@action(detail=True, methods=['post'])
def check(self, request, pk):
''' Endpoint: Check RSLang expression against schema context. '''
schema = models.PyConceptAdapter(self._get_schema())
serializer = serializers.ExpressionSerializer(data=request.data)
schema = m.PyConceptAdapter(self._get_schema())
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
result = pyconcept.check_expression(json.dumps(schema.data), expression)
@ -205,19 +226,19 @@ class RSFormViewSet(viewsets.ModelViewSet):
def resolve(self, request, pk):
''' Endpoint: Resolve refenrces in text against schema terms context. '''
schema = self._get_schema()
serializer = serializers.TextSerializer(data=request.data)
serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text']
resolver = schema.resolver()
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')
def export_trs(self, request, pk):
''' Endpoint: Download Exteor compatible file. '''
schema = serializers.RSFormTRSSerializer(self._get_schema()).data
schema = s.RSFormTRSSerializer(self._get_schema()).data
trs = utils.write_trs(schema)
filename = self._get_schema().alias
filename = self._get_schema().item.alias
if filename == '' or not filename.isascii():
# Note: non-ascii symbols in Content-Disposition
# are not supported by some browsers
@ -231,7 +252,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. '''
serializer_class = serializers.FileSerializer
serializer_class = s.FileSerializer
def post(self, request):
data = utils.read_trs(request.FILES['file'].file)
@ -239,10 +260,10 @@ class TrsImportView(views.APIView):
if owner.is_anonymous:
owner = None
_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)
schema = serializer.save()
result = serializers.RSFormMetaSerializer(schema)
result = s.LibraryItemSerializer(schema.item)
return Response(status=201, data=result.data)
@ -253,25 +274,27 @@ def create_rsform(request):
if owner.is_anonymous:
owner = None
if 'file' not in request.FILES:
serializer = serializers.RSFormMetaSerializer(data=request.data)
serializer = s.LibraryItemSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
schema = models.RSForm.objects.create(
schema = m.RSForm.create(
title=serializer.validated_data['title'],
owner=owner,
alias=serializer.validated_data.get('alias', ''),
comment=serializer.validated_data.get('comment', ''),
is_common=serializer.validated_data.get('is_common', False),
is_canonical=serializer.validated_data.get('is_canonical', False),
)
else:
data = utils.read_trs(request.FILES['file'].file)
_prepare_rsform_data(data, request, owner)
serializer = serializers.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True)
schema = serializer.save()
result = serializers.RSFormMetaSerializer(schema)
result = s.LibraryItemSerializer(schema.item)
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'] != '':
data['title'] = request.data['title']
if data['title'] == '':
@ -280,16 +303,21 @@ def _prepare_rsform_data(data: dict, request, owner: models.User):
data['alias'] = request.data['alias']
if 'comment' in request.data and request.data['comment'] != '':
data['comment'] = request.data['comment']
is_common = True
if 'is_common' in request.data:
is_common = request.data['is_common'] == 'true'
data['is_common'] = is_common
data['owner'] = owner
is_canonical = False
if 'is_canonical' in request.data:
is_canonical = request.data['is_canonical'] == 'true'
data['is_canonical'] = is_canonical
@api_view(['POST'])
def parse_expression(request):
''' Endpoint: Parse RS expression. '''
serializer = serializers.ExpressionSerializer(data=request.data)
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
result = pyconcept.parse_expression(expression)
@ -299,7 +327,7 @@ def parse_expression(request):
@api_view(['POST'])
def convert_to_ascii(request):
''' Endpoint: Convert expression to ASCII syntax. '''
serializer = serializers.ExpressionSerializer(data=request.data)
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression']
result = pyconcept.convert_to_ascii(expression)
@ -309,7 +337,7 @@ def convert_to_ascii(request):
@api_view(['POST'])
def convert_to_math(request):
''' Endpoint: Convert expression to MATH syntax. '''
serializer = serializers.ExpressionSerializer(data=request.data)
serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['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:
data = utils.read_trs(os.path.join(subdir, file))
data['is_common'] = True
data['is_canonical'] = True
serializer = RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer.is_valid(raise_exception=True)
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):
initial = True
@ -35,5 +29,4 @@ class Migration(migrations.Migration):
operations = [
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 { ErrorInfo } from '../components/BackendError';
import { DataCallback, deleteRSForm, getLibrary, postCloneRSForm, postNewRSForm } from '../utils/backendAPI';
import { ILibraryFilter, IRSFormCreateData, IRSFormData, IRSFormMeta, matchRSFormMeta } from '../utils/models';
import { DataCallback, deleteLibraryItem, getLibrary, postCloneLibraryItem, postNewRSForm } from '../utils/backendAPI';
import { ILibraryFilter, ILibraryItem, IRSFormCreateData, IRSFormData, matchLibraryItem } from '../utils/models';
import { useAuth } from './AuthContext';
interface ILibraryContext {
items: IRSFormMeta[]
items: ILibraryItem[]
loading: boolean
processing: boolean
error: ErrorInfo
setError: (error: ErrorInfo) => void
filter: (params: ILibraryFilter) => IRSFormMeta[]
createSchema: (data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => void
filter: (params: ILibraryFilter) => ILibraryItem[]
createSchema: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void
cloneSchema: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void
destroySchema: (target: number, callback?: () => void) => void
}
@ -34,7 +34,7 @@ interface LibraryStateProps {
}
export const LibraryState = ({ children }: LibraryStateProps) => {
const [ items, setItems ] = useState<IRSFormMeta[]>([])
const [ items, setItems ] = useState<ILibraryItem[]>([])
const [ loading, setLoading ] = useState(false);
const [ processing, setProcessing ] = useState(false);
const [ error, setError ] = useState<ErrorInfo>(undefined);
@ -44,13 +44,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
(params: ILibraryFilter) => {
let result = items;
if (params.ownedBy) {
result = result.filter(schema => schema.owner === params.ownedBy);
result = result.filter(item => item.owner === params.ownedBy);
}
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) {
result = result.filter(schema => matchRSFormMeta(params.queryMeta!, schema));
result = result.filter(item => matchLibraryItem(params.queryMeta!, item));
}
return result;
}, [items]);
@ -75,13 +75,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
}, [reload, user]);
const createSchema = useCallback(
(data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => {
(data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => {
setError(undefined);
postNewRSForm({
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error); },
onError: error => setError(error),
onSuccess: newSchema => {
reload();
if (callback) callback(newSchema);
@ -92,7 +92,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
const destroySchema = useCallback(
(target: number, callback?: () => void) => {
setError(undefined)
deleteRSForm(String(target), {
deleteLibraryItem(String(target), {
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
@ -108,7 +108,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
return;
}
setError(undefined)
postCloneRSForm(String(target), {
postCloneLibraryItem(String(target), {
data: data,
showError: true,
setLoading: setProcessing,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { TabList, TabPanel, Tabs } from 'react-tabs';
@ -35,7 +36,7 @@ function RSTabs() {
const navigate = useNavigate();
const search = useLocation().search;
const {
error, schema, loading,
error, schema, loading, claim, download,
cstCreate, cstDelete, cstRename
} = useRSForm();
const { destroySchema } = useLibrary();
@ -195,6 +196,35 @@ function RSTabs() {
});
}, [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 (
<div className='w-full'>
{ loading && <Loader /> }
@ -240,7 +270,10 @@ function RSTabs() {
>
<TabList className='flex items-start pl-2 select-none w-fit clr-bg-pop'>
<RSTabsMenu
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
showCloneDialog={() => setShowClone(true)}
showUploadDialog={() => setShowUpload(true)}
/>
@ -255,7 +288,10 @@ function RSTabs() {
<TabPanel className='flex items-start w-full gap-2 px-2'>
<EditorRSForm
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
/>
{schema.stats && <RSFormStats stats={schema.stats}/>}
</TabPanel>

View File

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

View File

@ -6,9 +6,8 @@ import { config } from './constants'
import {
IConstituentaList, IConstituentaMeta,
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData,
ICurrentUser, IExpressionParse, IReferenceData, IRefsText, IRSExpression,
IRSFormCreateData, IRSFormData,
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo,
ICurrentUser, IExpressionParse, ILibraryItem, ILibraryUpdateData, IReferenceData, IRefsText,
IRSExpression, IRSFormCreateData, IRSFormData, IRSFormUploadData, IUserInfo,
IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData, IUserUpdatePassword
} 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({
title: 'Available RSForms (Library) list',
endpoint: '/api/library/',
endpoint: '/api/library/active/',
request: request
});
}
export function postNewRSForm(request: FrontExchange<IRSFormCreateData, IRSFormMeta>) {
export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibraryItem>) {
AxiosPost({
title: 'New RSForm',
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({
title: 'clone RSForm',
endpoint: `/api/rsforms/${schema}/clone/`,
endpoint: `/api/library/${target}/clone/`,
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({
title: `RSForm id=${target}`,
endpoint: `/api/rsforms/${target}/`,
endpoint: `/api/library/${target}/`,
request: request
});
}
export function deleteRSForm(target: string, request: FrontAction) {
export function deleteLibraryItem(target: string, request: FrontAction) {
AxiosDelete({
title: `RSForm id=${target}`,
endpoint: `/api/rsforms/${target}/`,
endpoint: `/api/library/${target}/`,
request: request
});
}
export function postClaimRSForm(target: string, request: FrontPull<IRSFormMeta>) {
export function postClaimLibraryItem(target: string, request: FrontPull<ILibraryItem>) {
AxiosPost({
title: `Claim on RSForm id=${target}`,
endpoint: `/api/rsforms/${target}/claim/`,
endpoint: `/api/library/${target}/claim/`,
request: request
});
}

View File

@ -214,6 +214,29 @@ export interface ICstCreatedResponse {
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 ============
export interface IRSFormStats {
count_all: number
@ -233,28 +256,17 @@ export interface IRSFormStats {
count_theorem: number
}
export interface IRSForm {
id: number
title: string
alias: string
comment: string
is_common: boolean
time_create: string
time_update: string
owner: number | null
export interface IRSForm
extends ILibraryItem {
items: IConstituenta[]
stats: IRSFormStats
graph: 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
extends IRSFormUpdateData {
extends ILibraryUpdateData {
file?: File
fileName?: string
}
@ -454,7 +466,7 @@ export function matchConstituenta(query: string, target: IConstituenta, mode: Cs
return false;
}
export function matchRSFormMeta(query: string, target: IRSFormMeta) {
export function matchLibraryItem(query: string, target: ILibraryItem): boolean {
const queryI = query.toUpperCase();
if (target.alias.toUpperCase().match(queryI)) {
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);
}
});
}